From 8f51e4f3a7213393f309065132ccac5c49d02a59 Mon Sep 17 00:00:00 2001 From: noxzion Date: Sun, 15 Mar 2026 18:34:57 +0500 Subject: [PATCH 1/9] =?UTF-8?q?=D0=A7=D1=82=D0=BE-=D1=82=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=85=D0=BE=D0=B6=D0=B5=D0=B5=20=D0=BD=D0=B0=20=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/komet.png | Bin 0 -> 179772 bytes .../auth/code_confirmation_screen.dart | 228 ++++++++ lib/frontend/screens/auth/login_screen.dart | 491 ++++++++++++++++++ .../screens/chats/chat_list_screen.dart | 478 +++++++++++++++++ lib/main.dart | 113 +--- pubspec.lock | 210 +++++++- pubspec.yaml | 4 + 7 files changed, 1419 insertions(+), 105 deletions(-) create mode 100644 assets/komet.png create mode 100644 lib/frontend/screens/auth/code_confirmation_screen.dart diff --git a/assets/komet.png b/assets/komet.png new file mode 100644 index 0000000000000000000000000000000000000000..8c251b96dcf3e268f9cef5f2d2cd10745f77c2df GIT binary patch literal 179772 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 z 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 z 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 Vz1ZOo#>R(&oVj?q;-pW~{{zZv1@8a= literal 0 HcmV?d00001 diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart new file mode 100644 index 0000000..076f4f3 --- /dev/null +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -0,0 +1,228 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../chats/chat_list_screen.dart'; + +class CodeConfirmationScreen extends StatefulWidget { + final String phoneNumber; + + const CodeConfirmationScreen({super.key, required this.phoneNumber}); + + @override + State createState() => _CodeConfirmationScreenState(); +} + +class _CodeConfirmationScreenState extends State { + final TextEditingController _codeController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + int _timerSeconds = 30; + Timer? _timer; + + @override + void initState() { + super.initState(); + _startTimer(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _timer?.cancel(); + _codeController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _startTimer() { + _timer?.cancel(); + _timerSeconds = 30; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() { + if (_timerSeconds > 0) { + _timerSeconds--; + } else { + _timer?.cancel(); + } + }); + }); + } + + void _resendCode() { + if (_timerSeconds == 0) { + _startTimer(); + // TODO: Implement actual resend logic here + print('Resending code to ${widget.phoneNumber}'); + } + } + + void _navigateToChats() { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const ChatListScreen()), + (route) => false, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0D0D12), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white70), + onPressed: () => Navigator.pop(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + widget.phoneNumber, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + const SizedBox(height: 12), + const Text( + 'Мы отправили SMS с кодом подтверждения на ваш номер телефона.', + style: TextStyle( + color: Colors.white54, + fontSize: 15, + fontWeight: FontWeight.w400, + height: 1.4, + ), + ), + const SizedBox(height: 12), + Stack( + children: [ + Opacity( + opacity: 0, + child: SizedBox( + height: 0, + width: 0, + child: TextField( + controller: _codeController, + focusNode: _focusNode, + keyboardType: TextInputType.number, + autofillHints: const [AutofillHints.oneTimeCode], + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(5), + ], + onChanged: (value) { + setState(() {}); + if (value.length == 5) { + _navigateToChats(); + } + }, + ), + ), + ), + + GestureDetector( + onTap: () => _focusNode.requestFocus(), + child: FittedBox( + child: Row( + children: List.generate(5, (index) { + bool isFocused = _codeController.text.length == index && _focusNode.hasFocus; + bool hasValue = _codeController.text.length > index; + String char = hasValue ? _codeController.text[index] : ''; + + return Container( + width: 44, + height: 54, + margin: EdgeInsets.only(right: index == 4 ? 0 : 10), + decoration: BoxDecoration( + color: const Color(0xFF1E1E2A), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isFocused + ? const Color(0xFFBEC2FF) + : (hasValue ? Colors.white24 : Colors.transparent), + width: 1.5, + ), + ), + alignment: Alignment.center, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: Text( + char, + key: ValueKey(char + index.toString()), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + }), + ), + ), + ), + ], + ), + const SizedBox(height: 24), + GestureDetector( + onTap: _resendCode, + child: Text( + _timerSeconds > 0 + ? 'Отправить повторно через $_timerSeconds сек.' + : 'Отправить код по SMS', + style: const TextStyle( + color: Color(0xFF63A9F5), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: () { + if (_codeController.text.length == 5) { + _navigateToChats(); + } + }, + backgroundColor: _codeController.text.length == 5 + ? const Color(0xffc1c4ff) + : const Color(0xFF1E1E2A), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + child: Icon( + Icons.arrow_forward, + color: _codeController.text.length == 5 ? Colors.black : Colors.white24, + ), + ), + ], + ), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } +} diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index e69de29..298ebd3 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -0,0 +1,491 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'code_confirmation_screen.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final TextEditingController _phoneController = TextEditingController(); + String? _errorText; + + void _showTOS(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: const Color(0xFF1E1E2A), + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 1.0, + snap: true, + snapSizes: const [0.7, 1.0], + expand: false, + builder: (context, scrollController) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Условия использования', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Symbols.close, color: Colors.white70), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: ListView( + controller: scrollController, + children: const [ + Text( + 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' + '1. Статус и отношения\n' + '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' + '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' + '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' + '2. Условия использования\n' + '2.1. Используя Приложение «Komet», вы:\n' + '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' + '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' + '• Принимаете на себя все риски, связанные с использованием Приложения.\n' + '2.2. Строго запрещено:\n' + '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' + '• Осуществлять массовые рассылки (спам);\n' + '• Нарушать законодательство РФ и международное право;\n' + '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' + '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' + '2.4. Особенности технического взаимодействия:\n' + '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' + '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' + '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' + '3. Технические аспекты\n' + '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' + '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' + '3.3. Разработчики не несут ответственности за:\n' + '• Изменения в API оригинального сервиса;\n' + '• Блокировку аккаунтов пользователей;\n' + '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' + '4. Конфиденциальность\n' + '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' + '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' + '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' + '5. Ответственность и ограничения\n' + '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' + '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' + '6. Правовые основания\n' + '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' + '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' + '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' + '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' + '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' + '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' + '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' + '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' + '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' + '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' + '7. Заключительные положения\n' + '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' + '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' + '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' + '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' + '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' + '8. Функции безопасности и конфиденциальности\n' + '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' + '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' + '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' + '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' + '8.2. Данные функции:\n' + '• Направлены исключительно на защиту конфиденциальности пользователей;\n' + '• Не используются для обхода систем безопасности оригинального сервиса;\n' + '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' + '8.3. Разработчики не несут ответственности за:\n' + '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' + '• Изменения в работе сервиса при активации данных функций.\n' + '8.4. Функции экспорта и импорта сессии\n' + '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' + '• Обеспечения переносимости данных между устройствами пользователя\n' + '• Резервного копирования учетных данных\n' + '• Восстановления доступа при утере устройства\n' + '8.4.2. Особенности реализации:\n' + '• Экспорт сессии осуществляется без привязки к номеру телефона\n' + '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' + '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' + '8.4.3. Техническая реализация экспорта сессии:\n' + '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' + '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' + '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' + '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' + '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' + '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' + '8.4.4. Правовые основания:\n' + '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' + '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' + '• Принцип минимизации данных — сбор только необходимой для работы информации\n' + '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' + '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' + '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' + '8.4.5. Ограничения ответственности:\n' + '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' + '• Разработчики не имеют доступа к зашифрованным данным сессии\n' + '• Восстановление утерянных паролей невозможно в целях безопасности\n' + '• Ключи шифрования не хранятся в приложении и известны только пользователю', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _validateAndSubmit() { + setState(() { + final phone = _phoneController.text.replaceAll(RegExp(r'\D'), ''); + if (phone.isEmpty) { + _errorText = 'Пожалуйста, введите номер телефона'; + } else if (phone.length < 10) { + _errorText = 'Номер телефона слишком короткий'; + } else { + _errorText = null; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CodeConfirmationScreen( + phoneNumber: '+7 ${_phoneController.text}', + ), + ), + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0D0D12), + body: GestureDetector( + behavior: HitTestBehavior.translucent, + onVerticalDragEnd: (details) { + if (details.primaryVelocity != null && details.primaryVelocity! < -500) { + _showTOS(context); + } + }, + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Symbols.admin_panel_settings, + color: Colors.white70, + weight: 400, + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Symbols.language, + color: Colors.white70, + weight: 400, + ), + ), + ], + ), + const SizedBox(height: 24), + Center( + child: Column( + children: [ + Image.asset( + 'assets/komet.png', + height: 80, + color: Colors.white, + ), + const SizedBox(height: 16), + const Text( + 'Войдите в Komet', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + const Text( + 'Проверьте код страны и введите свой\nномер телефона.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 13, + fontWeight: FontWeight.w400, + height: 1.4, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + _buildInputField( + label: 'Страна', + content: Row( + children: [ + const Text( + 'Россия', + style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), + ), + const Spacer(), + const Icon(Icons.keyboard_arrow_down, color: Colors.white70), + ], + ), + ), + const SizedBox(height: 12), + _buildInputField( + label: 'Номер телефона', + isError: _errorText != null, + errorText: _errorText, + content: Row( + children: [ + const Text( + '+7', + style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), + ), + const SizedBox(width: 12), + Container( + width: 1, + height: 24, + color: Colors.white24, + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _PhoneInputFormatter(), + ], + style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), + decoration: const InputDecoration( + hintText: '(000) 000-00-00', + hintStyle: TextStyle(color: Colors.white38, fontSize: 15, fontWeight: FontWeight.w400), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (value) { + if (_errorText != null) { + setState(() { + _errorText = null; + }); + } + }, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + TextButton( + onPressed: () {}, + child: const Text( + 'Другие способы входа', + style: TextStyle( + color: Color(0xFFAFAFFF), + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.4, + ), + ), + ), + const Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: RichText( + text: TextSpan( + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + height: 1.4, + fontFamily: 'Outfit', + ), + children: [ + const TextSpan(text: 'Продолжая, вы соглашаетесь с\n'), + TextSpan( + text: 'пользовательскими соглашениями', + style: const TextStyle( + color: Color(0xFFAFAFFF), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showTOS(context), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: FloatingActionButton( + onPressed: _validateAndSubmit, + backgroundColor: const Color(0xffc1c4ff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + child: const Icon(Icons.arrow_forward, color: Colors.black), + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } + + + Widget _buildInputField({ + required String label, + required Widget content, + bool isError = false, + String? errorText, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: double.infinity, + height: 54, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + border: Border.all( + color: isError ? Colors.redAccent : const Color(0xFFBEC2FF), + width: 1.5, + ), + borderRadius: BorderRadius.circular(50), + ), + child: content, + ), + Positioned( + top: -10, + left: 20, + child: Container( + color: const Color(0xFF0D0D12), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + label, + style: TextStyle( + color: isError ? Colors.redAccent : const Color(0xFFBEC2FF), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + if (isError && errorText != null) ...[ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + errorText, + style: const TextStyle( + color: Colors.redAccent, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ], + ); + } +} + +class _PhoneInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + var text = newValue.text.replaceAll(RegExp(r'\D'), ''); + + if (newValue.text.length < oldValue.text.length) { + final oldDigits = oldValue.text.replaceAll(RegExp(r'\D'), ''); + if (text.length == oldDigits.length && text.isNotEmpty) { + text = text.substring(0, text.length - 1); + } + } + + if (text.length > 10) text = text.substring(0, 10); + + final buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + if (i == 0) buffer.write('('); + buffer.write(text[i]); + if (i == 2) buffer.write(') '); + if (i == 5) buffer.write('-'); + } + + final formattedText = buffer.toString(); + return TextEditingValue( + text: formattedText, + selection: TextSelection.collapsed(offset: formattedText.length), + ); + } +} diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index e69de29..8288825 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -0,0 +1,478 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'dart:ui' as ui; + +class ChatListScreen extends StatefulWidget { + const ChatListScreen({super.key}); + + @override + State createState() => _ChatListScreenState(); +} + +class _ChatListScreenState extends State { + String _selectedCategory = 'Все чаты'; + int _currentNavIndex = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0D0D12), + body: SafeArea( + bottom: false, + child: Stack( + children: [ + CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Подключение...', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Symbols.more_vert, + color: Colors.white54, + weight: 400, + ), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 96, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + _buildStoryItem('Даша', 'https://i.pravatar.cc/150?u=dasha', true), + _buildStoryItem('Мастика', 'https://i.pravatar.cc/150?u=mastika', false), + ], + ), + ), + ), + + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + sliver: SliverToBoxAdapter( + child: Container( + height: 44, + decoration: BoxDecoration( + color: const Color(0xFF1E1E2A), + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: const Row( + children: [ + Icon(Symbols.search, color: Colors.white38, size: 20, weight: 400), + SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle(color: Colors.white, fontSize: 15), + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle(color: Colors.white38, fontSize: 15), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ), + ), + ), + + SliverToBoxAdapter( + child: SizedBox( + height: 48, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + ui.PointerDeviceKind.trackpad, + }, + ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + physics: const BouncingScrollPhysics(), + children: [ + _buildCategoryChip('Все чаты'), + const SizedBox(width: 8), + _buildCategoryChip('Контакты'), + const SizedBox(width: 8), + _buildCategoryChip('Пидоры'), + const SizedBox(width: 8), + _buildCategoryChip('Каналы'), + const SizedBox(width: 8), + _buildCategoryChip('Группы'), + const SizedBox(width: 8), + _buildCategoryChip('Боты'), + const SizedBox(width: 8), + _buildCategoryChip('Избранное'), + const SizedBox(width: 8), + _buildCategoryChip('Архив'), + ], + ), + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate([ + _buildChatItem( + 'Станислав', + 'Хорошо', + '10:07', + 'https://i.pravatar.cc/150?u=stas', + isOnline: true, + isRead: true, + ), + _buildChatItem( + 'Илья', + 'печатает...', + '10:07', + 'https://i.pravatar.cc/150?u=ilya', + isOnline: true, + isTyping: true, + unreadCount: 1, + ), + _buildChatItem( + 'Вероника', + 'Спасибо', + '09:56', + 'https://i.pravatar.cc/150?u=veronika', + isRead: true, + ), + _buildChatItem( + 'Komet Client', + 'Кстати. Смотрите, какую шту...', + '09:56', + 'https://i.pravatar.cc/150?u=komet', + unreadCount: 5, + isMuted: true, + ), + _buildChatItem( + '4-й подъезд', + 'Людмила: Сколько?', + '09:34', + 'https://i.pravatar.cc/150?u=podezd', + unreadCount: 78, + isMuted: true, + ), + const SizedBox(height: 100), + ]), + ), + ], + ), + Positioned( + left: 8, + right: 8, + bottom: 24, + child: Container( + height: 68, + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: const Color(0xFF1E1E28), + borderRadius: BorderRadius.circular(34), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + double totalWidth = constraints.maxWidth; + + double totalWeight = 5.2; + double unitWidth = totalWidth / totalWeight; + double activeWidth = unitWidth * 2.2; + double inactiveWidth = unitWidth * 1.0; + + double leftOffset = 0; + for (int i = 0; i < _currentNavIndex; i++) { + leftOffset += inactiveWidth; + } + + return Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + left: leftOffset + 4, + top: 8, + bottom: 8, + width: activeWidth - 8, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFBEC2FF), + borderRadius: BorderRadius.circular(26), + ), + ), + ), + Row( + children: List.generate(4, (index) { + IconData icon; + String label; + switch (index) { + case 0: icon = Symbols.chat_bubble; label = 'Чаты'; break; + case 1: icon = Symbols.call; label = 'Звонки'; break; + case 2: icon = Symbols.person_pin; label = 'Контакты'; break; + default: icon = Symbols.settings; label = 'Настройки'; + } + + bool isSelected = _currentNavIndex == index; + return AnimatedContainer( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + width: isSelected ? (activeWidth - 0.5) : (inactiveWidth - 0.5), + child: ClipRRect( + borderRadius: BorderRadius.circular(26), + child: _buildNavItem(index, icon, label), + ), + ); + }), + ), + ], + ); + }, + ), + ), + ), + Positioned( + right: 20, + bottom: 110, + child: FloatingActionButton( + onPressed: () {}, + backgroundColor: const Color(0xFFC1C4FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: const Icon(Symbols.add, color: Colors.black, size: 28, weight: 400), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStoryItem(String name, String imageUrl, bool hasUpdate) { + return Padding( + padding: const EdgeInsets.only(right: 16), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(2.5), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: hasUpdate + ? Border.all(color: const Color(0xFFC1C4FF), width: 2) + : Border.all(color: Colors.white10), + ), + child: CircleAvatar( + radius: 26, + backgroundImage: NetworkImage(imageUrl), + ), + ), + const SizedBox(height: 6), + Text( + name, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildCategoryChip(String title) { + bool isSelected = _selectedCategory == title; + return GestureDetector( + onTap: () => setState(() => _selectedCategory = title), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFFC1C4FF) : const Color(0xFF1E1E2A), + borderRadius: BorderRadius.circular(15), + ), + child: Text( + title, + style: TextStyle( + color: isSelected ? Colors.black : Colors.white54, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + Widget _buildChatItem( + String name, + String message, + String time, + String imageUrl, { + bool isOnline = false, + bool isTyping = false, + bool isRead = false, + int unreadCount = 0, + bool isMuted = false, + }) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), + leading: Stack( + children: [ + CircleAvatar( + radius: 24, + backgroundImage: NetworkImage(imageUrl), + ), + if (isOnline) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: const Color(0xFFC1C4FF), + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFF0D0D12), width: 2), + ), + ), + ), + ], + ), + title: Row( + children: [ + Expanded( + child: Text( + name, + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + if (isMuted) + const Icon(Symbols.notifications_off, color: Colors.white24, size: 14, weight: 400), + const SizedBox(width: 8), + Text( + time, + style: const TextStyle( + color: Colors.white38, + fontSize: 12, + ), + ), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Expanded( + child: Text( + message, + style: TextStyle( + color: isTyping ? const Color(0xFFC1C4FF) : Colors.white54, + fontSize: 14, + fontWeight: isTyping ? FontWeight.w500 : FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (unreadCount > 0) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: isMuted ? Colors.white10 : const Color(0xFF1E1E2A), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + unreadCount.toString(), + style: TextStyle( + color: isMuted ? Colors.white38 : Colors.white, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ) + else if (isRead) + const Icon(Symbols.done_all, color: Color(0xFFC1C4FF), size: 16, weight: 400), + ], + ), + ), + ); + } + + Widget _buildNavItem(int index, IconData icon, String label) { + bool isSelected = _currentNavIndex == index; + return GestureDetector( + onTap: () => setState(() => _currentNavIndex = index), + behavior: HitTestBehavior.opaque, + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: isSelected ? Colors.black : Colors.white, + size: 20, + weight: 400, + fill: isSelected ? 1.0 : 0.0, + ), + AnimatedContainer( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + width: isSelected ? null : 0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isSelected ? 1.0 : 0.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 244a702..8c52477 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:komet/frontend/screens/auth/login_screen.dart'; void main() { runApp(const MyApp()); @@ -7,116 +9,19 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Komet', + debugShowCheckedModeBanner: false, theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: .center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + useMaterial3: true, + brightness: Brightness.dark, + textTheme: GoogleFonts.outfitTextTheme( + ThemeData.dark().textTheme, ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), + home: const LoginScreen(), ); } } diff --git a/pubspec.lock b/pubspec.lock index 3e29bd2..150913d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -65,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -83,6 +115,46 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -123,6 +195,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -139,6 +219,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + url: "https://pub.dev" + source: hosted + version: "4.2906.0" meta: dependency: transitive description: @@ -155,6 +243,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" path: dependency: transitive description: @@ -163,6 +267,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + 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: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + 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" + 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" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -216,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -232,6 +416,30 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.4 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index a704a79..b832e79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: dart_lz4: ^1.0.0 msgpack_dart: ^1.0.1 logger: ^2.6.2 + google_fonts: ^6.2.1 + material_symbols_icons: ^4.2906.0 dev_dependencies: flutter_test: @@ -59,6 +61,8 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + assets: + - assets/komet.png # To add assets to your application, add an assets section, like this: # assets: From a72c9b40f90d14da3b5aba9ca97bf306177a2099 Mon Sep 17 00:00:00 2001 From: noxzion Date: Sun, 15 Mar 2026 20:08:38 +0500 Subject: [PATCH 2/9] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/chats/chat_list_screen.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 8288825..d2e7c66 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -112,21 +112,21 @@ class _ChatListScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), physics: const BouncingScrollPhysics(), children: [ - _buildCategoryChip('Все чаты'), + _buildFolderChip('Все чаты'), const SizedBox(width: 8), - _buildCategoryChip('Контакты'), + _buildFolderChip('Контакты'), const SizedBox(width: 8), - _buildCategoryChip('Пидоры'), + _buildFolderChip('Пидоры'), const SizedBox(width: 8), - _buildCategoryChip('Каналы'), + _buildFolderChip('Каналы'), const SizedBox(width: 8), - _buildCategoryChip('Группы'), + _buildFolderChip('Группы'), const SizedBox(width: 8), - _buildCategoryChip('Боты'), + _buildFolderChip('Боты'), const SizedBox(width: 8), - _buildCategoryChip('Избранное'), + _buildFolderChip('Избранное'), const SizedBox(width: 8), - _buildCategoryChip('Архив'), + _buildFolderChip('Архив'), ], ), ), @@ -306,7 +306,7 @@ class _ChatListScreenState extends State { ); } - Widget _buildCategoryChip(String title) { + Widget _buildFolderChip(String title) { bool isSelected = _selectedCategory == title; return GestureDetector( onTap: () => setState(() => _selectedCategory = title), From 0508fe4f7fd60bfa77b1e90e6813abb030739639 Mon Sep 17 00:00:00 2001 From: noxzion Date: Sun, 15 Mar 2026 21:13:43 +0500 Subject: [PATCH 3/9] =?UTF-8?q?=D0=9F=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F=D0=BB?= =?UTF-8?q?=20=D1=88=D1=80=D0=B8=D1=84=D1=82=D1=8B=20=D0=B2=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=85,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/code_confirmation_screen.dart | 6 +- lib/frontend/screens/auth/login_screen.dart | 260 +++++++++++------- lib/main.dart | 2 +- 3 files changed, 168 insertions(+), 100 deletions(-) diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 076f4f3..39b828b 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../chats/chat_list_screen.dart'; @@ -87,11 +88,10 @@ class _CodeConfirmationScreenState extends State { const SizedBox(height: 16), Text( widget.phoneNumber, - style: const TextStyle( + style: GoogleFonts.inter( color: Colors.white, fontSize: 22, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontWeight: FontWeight.w500, ), ), const SizedBox(height: 12), diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index 298ebd3..700742c 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'code_confirmation_screen.dart'; @@ -13,7 +14,7 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final TextEditingController _phoneController = TextEditingController(); - String? _errorText; + bool _isPhoneValid = false; void _showTOS(BuildContext context) { showModalBottomSheet( @@ -38,12 +39,12 @@ class _LoginScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Условия использования', - style: TextStyle( + style: GoogleFonts.inter( color: Colors.white, fontSize: 20, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, ), ), IconButton( @@ -166,25 +167,102 @@ class _LoginScreenState extends State { ); } - void _validateAndSubmit() { - setState(() { - final phone = _phoneController.text.replaceAll(RegExp(r'\D'), ''); - if (phone.isEmpty) { - _errorText = 'Пожалуйста, введите номер телефона'; - } else if (phone.length < 10) { - _errorText = 'Номер телефона слишком короткий'; - } else { - _errorText = null; - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CodeConfirmationScreen( - phoneNumber: '+7 ${_phoneController.text}', + void _showPhoneConfirmationDialog(String formattedPhone) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: '', + barrierColor: Colors.black54, + transitionDuration: const Duration(milliseconds: 250), + pageBuilder: (context, anim1, anim2) => const SizedBox.shrink(), + transitionBuilder: (context, anim1, anim2, child) { + final curve = Curves.easeOutQuart.transform(anim1.value); + return Opacity( + opacity: anim1.value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - curve)), + child: Transform.scale( + scale: 0.8 + (0.2 * curve), + child: AlertDialog( + backgroundColor: const Color(0xFF1E1E2A), + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + actionsPadding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Это правильный номер?', + style: GoogleFonts.inter( + color: Colors.white70, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + Text( + '+7 $formattedPhone', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text( + 'Изменить', + style: TextStyle( + color: Color(0xFFAFAFFF), + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CodeConfirmationScreen( + phoneNumber: '+7 $formattedPhone', + ), + ), + ); + }, + child: const Text( + 'Готово', + style: TextStyle( + color: Color(0xFFAFAFFF), + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), ), ), ); - } - }); + }, + ); + } + + void _validateAndSubmit() { + _showPhoneConfirmationDialog(_phoneController.text); } @override @@ -242,9 +320,9 @@ class _LoginScreenState extends State { color: Colors.white, ), const SizedBox(height: 16), - const Text( + Text( 'Войдите в Komet', - style: TextStyle( + style: GoogleFonts.inter( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w500, @@ -279,58 +357,55 @@ class _LoginScreenState extends State { ), ), const SizedBox(height: 12), - _buildInputField( - label: 'Номер телефона', - isError: _errorText != null, - errorText: _errorText, - content: Row( - children: [ - const Text( - '+7', - style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), - ), - const SizedBox(width: 12), - Container( - width: 1, - height: 24, - color: Colors.white24, - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _phoneController, - keyboardType: TextInputType.phone, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _PhoneInputFormatter(), - ], - style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), - decoration: const InputDecoration( - hintText: '(000) 000-00-00', - hintStyle: TextStyle(color: Colors.white38, fontSize: 15, fontWeight: FontWeight.w400), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - onChanged: (value) { - if (_errorText != null) { - setState(() { - _errorText = null; - }); - } - }, + _buildInputField( + label: 'Номер телефона', + content: Row( + children: [ + const Text( + '+7', + style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), ), - ), - ], + const SizedBox(width: 12), + Container( + width: 1, + height: 24, + color: Colors.white24, + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _PhoneInputFormatter(), + ], + style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), + decoration: const InputDecoration( + hintText: '(000) 000-00-00', + hintStyle: TextStyle(color: Colors.white38, fontSize: 15, fontWeight: FontWeight.w400), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (value) { + final digits = value.replaceAll(RegExp(r'\D'), ''); + setState(() { + _isPhoneValid = digits.length == 10; + }); + }, + ), + ), + ], + ), ), - ), const SizedBox(height: 24), TextButton( onPressed: () {}, - child: const Text( + child: Text( 'Другие способы входа', - style: TextStyle( - color: Color(0xFFAFAFFF), + style: GoogleFonts.inter( + color: const Color(0xFFAFAFFF), fontSize: 14, fontWeight: FontWeight.w400, height: 1.4, @@ -346,20 +421,29 @@ class _LoginScreenState extends State { padding: const EdgeInsets.only(bottom: 24.0), child: RichText( text: TextSpan( - style: const TextStyle( + style: GoogleFonts.inter( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w500, height: 1.4, - fontFamily: 'Outfit', ), children: [ - const TextSpan(text: 'Продолжая, вы соглашаетесь с\n'), + TextSpan( + text: 'Продолжая, вы соглашаетесь с \n', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), TextSpan( text: 'пользовательскими соглашениями', - style: const TextStyle( - color: Color(0xFFAFAFFF), - fontWeight: FontWeight.w500, + style: GoogleFonts.inter( + color: const Color(0xFFAFAFFF), + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, ), recognizer: TapGestureRecognizer() ..onTap = () => _showTOS(context), @@ -372,12 +456,12 @@ class _LoginScreenState extends State { Padding( padding: const EdgeInsets.only(bottom: 16.0), child: FloatingActionButton( - onPressed: _validateAndSubmit, - backgroundColor: const Color(0xffc1c4ff), + onPressed: _isPhoneValid ? _validateAndSubmit : null, + backgroundColor: _isPhoneValid ? const Color(0xffc1c4ff) : Colors.white10, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), - child: const Icon(Icons.arrow_forward, color: Colors.black), + child: Icon(Icons.arrow_forward, color: _isPhoneValid ? Colors.black : Colors.white24), ), ), ], @@ -400,8 +484,6 @@ class _LoginScreenState extends State { Widget _buildInputField({ required String label, required Widget content, - bool isError = false, - String? errorText, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -415,7 +497,7 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( border: Border.all( - color: isError ? Colors.redAccent : const Color(0xFFBEC2FF), + color: const Color(0xFFBEC2FF), width: 1.5, ), borderRadius: BorderRadius.circular(50), @@ -430,8 +512,8 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( label, - style: TextStyle( - color: isError ? Colors.redAccent : const Color(0xFFBEC2FF), + style: const TextStyle( + color: Color(0xFFBEC2FF), fontSize: 14, fontWeight: FontWeight.w400, ), @@ -440,20 +522,6 @@ class _LoginScreenState extends State { ), ], ), - if (isError && errorText != null) ...[ - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - errorText, - style: const TextStyle( - color: Colors.redAccent, - fontSize: 12, - fontWeight: FontWeight.w400, - ), - ), - ), - ], ], ); } diff --git a/lib/main.dart b/lib/main.dart index 8c52477..f273ad7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { theme: ThemeData( useMaterial3: true, brightness: Brightness.dark, - textTheme: GoogleFonts.outfitTextTheme( + textTheme: GoogleFonts.interTextTheme( ThemeData.dark().textTheme, ), ), From 65010cccfc26f5d9b485efca6a490b7fa0ae471b Mon Sep 17 00:00:00 2001 From: noxzion Date: Mon, 16 Mar 2026 08:09:19 +0500 Subject: [PATCH 4/9] =?UTF-8?q?=D0=92=D1=8B=D0=B1=D0=B5=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/config/countries.dart | 472 ++++++++++++++++++ .../auth/code_confirmation_screen.dart | 1 - lib/frontend/screens/auth/login_screen.dart | 198 ++++++-- .../screens/chats/chat_list_screen.dart | 6 +- lib/main.dart | 2 +- pubspec.lock | 56 +++ 6 files changed, 689 insertions(+), 46 deletions(-) create mode 100644 lib/core/config/countries.dart diff --git a/lib/core/config/countries.dart b/lib/core/config/countries.dart new file mode 100644 index 0000000..171126d --- /dev/null +++ b/lib/core/config/countries.dart @@ -0,0 +1,472 @@ +import 'dart:convert'; + +class CountryName { + final String code; // ISO 3166-1 alpha-2, например "RU" + final String en; + final String ru; + final String phoneCode; // Код страны для звонков, например "+7" + final int phoneDigits; // Количество цифр номера после кода страны + final String phoneMask; // Маска абонентского номера, например "(###) ###-##-##" + final List phoneGroupSizes; // Размеры групп цифр, например [3, 3, 2, 2] + final List phoneGroupSeparators; // Разделители вокруг групп, например ["(", ") ", "-", "-", ""] + + const CountryName({ + required this.code, + required this.en, + required this.ru, + required this.phoneCode, + required this.phoneDigits, + required this.phoneMask, + required this.phoneGroupSizes, + required this.phoneGroupSeparators, + }); +} + +/// Полный список стран (195 государств) с названием на русском и английском. +/// +/// Источник данных: +/// https://github.com/stefangabos/world_countries +final List allCountries = _buildCountries(); + +/// Быстрый доступ по ISO-коду. +final Map countriesByCode = { + for (final country in allCountries) country.code: country, +}; + +/// Пример использования: +/// ```dart +/// final country = exampleCountryLookup('RU'); +/// print(country?['ru']); // Россия +/// print(country?['en']); // Russian Federation +/// ``` +Map? exampleCountryLookup(String code) { + final country = countriesByCode[code.toUpperCase()]; + if (country == null) return null; + + return { + 'code': country.code, + 'ru': country.ru, + 'en': country.en, + }; +} + +List _buildCountries() { + final enList = (jsonDecode(_countriesEnJson) as List) + .cast>(); + final ruList = (jsonDecode(_countriesRuJson) as List) + .cast>(); + + final ruByCode = { + for (final item in ruList) + (item['alpha2'] as String).toUpperCase(): item['name'] as String, + }; + + final countries = enList.map((item) { + final code = (item['alpha2'] as String).toUpperCase(); + final en = item['name'] as String; + final ru = ruByCode[code] ?? en; + + final phoneCode = item['phoneCode'] as String; + final phoneDigits = item['phoneDigits'] as int; + final phoneMask = item['phoneMask'] as String; + final phoneGroupSizes = (item['phoneGroupSizes'] as List).cast(); + final phoneGroupSeparators = (item['phoneGroupSeparators'] as List).cast(); + + return CountryName(code: code, en: en, ru: ru, phoneCode: phoneCode, phoneDigits: phoneDigits, phoneMask: phoneMask, phoneGroupSizes: phoneGroupSizes, phoneGroupSeparators: phoneGroupSeparators); + }).toList(); + + countries.sort((a, b) => a.en.compareTo(b.en)); + return countries; +} + +const String _countriesEnJson = '''[ +{"id": 4, "alpha2": "af", "alpha3": "afg", "name": "Afghanistan", "phoneCode": "+93", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 8, "alpha2": "al", "alpha3": "alb", "name": "Albania", "phoneCode": "+355", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 12, "alpha2": "dz", "alpha3": "dza", "name": "Algeria", "phoneCode": "+213", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 20, "alpha2": "ad", "alpha3": "and", "name": "Andorra", "phoneCode": "+376", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]}, +{"id": 24, "alpha2": "ao", "alpha3": "ago", "name": "Angola", "phoneCode": "+244", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 28, "alpha2": "ag", "alpha3": "atg", "name": "Antigua and Barbuda", "phoneCode": "+1268", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 32, "alpha2": "ar", "alpha3": "arg", "name": "Argentina", "phoneCode": "+54", "phoneDigits": 10, "phoneMask": "(##) ####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 51, "alpha2": "am", "alpha3": "arm", "name": "Armenia", "phoneCode": "+374", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 36, "alpha2": "au", "alpha3": "aus", "name": "Australia", "phoneCode": "+61", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 40, "alpha2": "at", "alpha3": "aut", "name": "Austria", "phoneCode": "+43", "phoneDigits": 10, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 31, "alpha2": "az", "alpha3": "aze", "name": "Azerbaijan", "phoneCode": "+994", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 44, "alpha2": "bs", "alpha3": "bhs", "name": "Bahamas", "phoneCode": "+1242", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 48, "alpha2": "bh", "alpha3": "bhr", "name": "Bahrain", "phoneCode": "+973", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 50, "alpha2": "bd", "alpha3": "bgd", "name": "Bangladesh", "phoneCode": "+880", "phoneDigits": 10, "phoneMask": "####-######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 52, "alpha2": "bb", "alpha3": "brb", "name": "Barbados", "phoneCode": "+1246", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 112, "alpha2": "by", "alpha3": "blr", "name": "Belarus", "phoneCode": "+375", "phoneDigits": 9, "phoneMask": "## ###-##-##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", "-", "-", ""]}, +{"id": 56, "alpha2": "be", "alpha3": "bel", "name": "Belgium", "phoneCode": "+32", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 84, "alpha2": "bz", "alpha3": "blz", "name": "Belize", "phoneCode": "+501", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 204, "alpha2": "bj", "alpha3": "ben", "name": "Benin", "phoneCode": "+229", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 64, "alpha2": "bt", "alpha3": "btn", "name": "Bhutan", "phoneCode": "+975", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 68, "alpha2": "bo", "alpha3": "bol", "name": "Bolivia, Plurinational State of", "phoneCode": "+591", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]}, +{"id": 70, "alpha2": "ba", "alpha3": "bih", "name": "Bosnia and Herzegovina", "phoneCode": "+387", "phoneDigits": 8, "phoneMask": "## ###-###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", "-", ""]}, +{"id": 72, "alpha2": "bw", "alpha3": "bwa", "name": "Botswana", "phoneCode": "+267", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 76, "alpha2": "br", "alpha3": "bra", "name": "Brazil", "phoneCode": "+55", "phoneDigits": 11, "phoneMask": "(##) #####-####", "phoneGroupSizes": [2, 5, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 96, "alpha2": "bn", "alpha3": "brn", "name": "Brunei Darussalam", "phoneCode": "+673", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 100, "alpha2": "bg", "alpha3": "bgr", "name": "Bulgaria", "phoneCode": "+359", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 854, "alpha2": "bf", "alpha3": "bfa", "name": "Burkina Faso", "phoneCode": "+226", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 108, "alpha2": "bi", "alpha3": "bdi", "name": "Burundi", "phoneCode": "+257", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 132, "alpha2": "cv", "alpha3": "cpv", "name": "Cabo Verde", "phoneCode": "+238", "phoneDigits": 7, "phoneMask": "### ## ##", "phoneGroupSizes": [3, 2, 2], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 116, "alpha2": "kh", "alpha3": "khm", "name": "Cambodia", "phoneCode": "+855", "phoneDigits": 9, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 120, "alpha2": "cm", "alpha3": "cmr", "name": "Cameroon", "phoneCode": "+237", "phoneDigits": 9, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 124, "alpha2": "ca", "alpha3": "can", "name": "Canada", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 140, "alpha2": "cf", "alpha3": "caf", "name": "Central African Republic", "phoneCode": "+236", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 148, "alpha2": "td", "alpha3": "tcd", "name": "Chad", "phoneCode": "+235", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 152, "alpha2": "cl", "alpha3": "chl", "name": "Chile", "phoneCode": "+56", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 156, "alpha2": "cn", "alpha3": "chn", "name": "China", "phoneCode": "+86", "phoneDigits": 11, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 170, "alpha2": "co", "alpha3": "col", "name": "Colombia", "phoneCode": "+57", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 174, "alpha2": "km", "alpha3": "com", "name": "Comoros", "phoneCode": "+269", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 178, "alpha2": "cg", "alpha3": "cog", "name": "Congo", "phoneCode": "+242", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 180, "alpha2": "cd", "alpha3": "cod", "name": "Congo, Democratic Republic of the", "phoneCode": "+243", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 188, "alpha2": "cr", "alpha3": "cri", "name": "Costa Rica", "phoneCode": "+506", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 384, "alpha2": "ci", "alpha3": "civ", "name": "Côte d'Ivoire", "phoneCode": "+225", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 191, "alpha2": "hr", "alpha3": "hrv", "name": "Croatia", "phoneCode": "+385", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 192, "alpha2": "cu", "alpha3": "cub", "name": "Cuba", "phoneCode": "+53", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]}, +{"id": 196, "alpha2": "cy", "alpha3": "cyp", "name": "Cyprus", "phoneCode": "+357", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 203, "alpha2": "cz", "alpha3": "cze", "name": "Czechia", "phoneCode": "+420", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 208, "alpha2": "dk", "alpha3": "dnk", "name": "Denmark", "phoneCode": "+45", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 262, "alpha2": "dj", "alpha3": "dji", "name": "Djibouti", "phoneCode": "+253", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 212, "alpha2": "dm", "alpha3": "dma", "name": "Dominica", "phoneCode": "+1767", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 214, "alpha2": "do", "alpha3": "dom", "name": "Dominican Republic", "phoneCode": "+1809", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 218, "alpha2": "ec", "alpha3": "ecu", "name": "Ecuador", "phoneCode": "+593", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 818, "alpha2": "eg", "alpha3": "egy", "name": "Egypt", "phoneCode": "+20", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 222, "alpha2": "sv", "alpha3": "slv", "name": "El Salvador", "phoneCode": "+503", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 226, "alpha2": "gq", "alpha3": "gnq", "name": "Equatorial Guinea", "phoneCode": "+240", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 232, "alpha2": "er", "alpha3": "eri", "name": "Eritrea", "phoneCode": "+291", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 233, "alpha2": "ee", "alpha3": "est", "name": "Estonia", "phoneCode": "+372", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 748, "alpha2": "sz", "alpha3": "swz", "name": "Eswatini", "phoneCode": "+268", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 231, "alpha2": "et", "alpha3": "eth", "name": "Ethiopia", "phoneCode": "+251", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 242, "alpha2": "fj", "alpha3": "fji", "name": "Fiji", "phoneCode": "+679", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 246, "alpha2": "fi", "alpha3": "fin", "name": "Finland", "phoneCode": "+358", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 250, "alpha2": "fr", "alpha3": "fra", "name": "France", "phoneCode": "+33", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 266, "alpha2": "ga", "alpha3": "gab", "name": "Gabon", "phoneCode": "+241", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 270, "alpha2": "gm", "alpha3": "gmb", "name": "Gambia", "phoneCode": "+220", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 268, "alpha2": "ge", "alpha3": "geo", "name": "Georgia", "phoneCode": "+995", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 276, "alpha2": "de", "alpha3": "deu", "name": "Germany", "phoneCode": "+49", "phoneDigits": 10, "phoneMask": "#### #######", "phoneGroupSizes": [4, 7], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 288, "alpha2": "gh", "alpha3": "gha", "name": "Ghana", "phoneCode": "+233", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 300, "alpha2": "gr", "alpha3": "grc", "name": "Greece", "phoneCode": "+30", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 308, "alpha2": "gd", "alpha3": "grd", "name": "Grenada", "phoneCode": "+1473", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 320, "alpha2": "gt", "alpha3": "gtm", "name": "Guatemala", "phoneCode": "+502", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 324, "alpha2": "gn", "alpha3": "gin", "name": "Guinea", "phoneCode": "+224", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 624, "alpha2": "gw", "alpha3": "gnb", "name": "Guinea-Bissau", "phoneCode": "+245", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 328, "alpha2": "gy", "alpha3": "guy", "name": "Guyana", "phoneCode": "+592", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 332, "alpha2": "ht", "alpha3": "hti", "name": "Haiti", "phoneCode": "+509", "phoneDigits": 8, "phoneMask": "## ## ####", "phoneGroupSizes": [2, 2, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 340, "alpha2": "hn", "alpha3": "hnd", "name": "Honduras", "phoneCode": "+504", "phoneDigits": 8, "phoneMask": "####-####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 348, "alpha2": "hu", "alpha3": "hun", "name": "Hungary", "phoneCode": "+36", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 352, "alpha2": "is", "alpha3": "isl", "name": "Iceland", "phoneCode": "+354", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 356, "alpha2": "in", "alpha3": "ind", "name": "India", "phoneCode": "+91", "phoneDigits": 10, "phoneMask": "##### #####", "phoneGroupSizes": [5, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 360, "alpha2": "id", "alpha3": "idn", "name": "Indonesia", "phoneCode": "+62", "phoneDigits": 10, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 364, "alpha2": "ir", "alpha3": "irn", "name": "Iran, Islamic Republic of", "phoneCode": "+98", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 368, "alpha2": "iq", "alpha3": "irq", "name": "Iraq", "phoneCode": "+964", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 372, "alpha2": "ie", "alpha3": "irl", "name": "Ireland", "phoneCode": "+353", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 376, "alpha2": "il", "alpha3": "isr", "name": "Israel", "phoneCode": "+972", "phoneDigits": 9, "phoneMask": "##-###-####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 380, "alpha2": "it", "alpha3": "ita", "name": "Italy", "phoneCode": "+39", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 388, "alpha2": "jm", "alpha3": "jam", "name": "Jamaica", "phoneCode": "+1876", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 392, "alpha2": "jp", "alpha3": "jpn", "name": "Japan", "phoneCode": "+81", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 400, "alpha2": "jo", "alpha3": "jor", "name": "Jordan", "phoneCode": "+962", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 398, "alpha2": "kz", "alpha3": "kaz", "name": "Kazakhstan", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]}, +{"id": 404, "alpha2": "ke", "alpha3": "ken", "name": "Kenya", "phoneCode": "+254", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 296, "alpha2": "ki", "alpha3": "kir", "name": "Kiribati", "phoneCode": "+686", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 408, "alpha2": "kp", "alpha3": "prk", "name": "Korea, Democratic People's Republic of", "phoneCode": "+850", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 410, "alpha2": "kr", "alpha3": "kor", "name": "Korea, Republic of", "phoneCode": "+82", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 414, "alpha2": "kw", "alpha3": "kwt", "name": "Kuwait", "phoneCode": "+965", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 417, "alpha2": "kg", "alpha3": "kgz", "name": "Kyrgyzstan", "phoneCode": "+996", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 418, "alpha2": "la", "alpha3": "lao", "name": "Lao People's Democratic Republic", "phoneCode": "+856", "phoneDigits": 9, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 428, "alpha2": "lv", "alpha3": "lva", "name": "Latvia", "phoneCode": "+371", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 422, "alpha2": "lb", "alpha3": "lbn", "name": "Lebanon", "phoneCode": "+961", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 426, "alpha2": "ls", "alpha3": "lso", "name": "Lesotho", "phoneCode": "+266", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 430, "alpha2": "lr", "alpha3": "lbr", "name": "Liberia", "phoneCode": "+231", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 434, "alpha2": "ly", "alpha3": "lby", "name": "Libya", "phoneCode": "+218", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 438, "alpha2": "li", "alpha3": "lie", "name": "Liechtenstein", "phoneCode": "+423", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 440, "alpha2": "lt", "alpha3": "ltu", "name": "Lithuania", "phoneCode": "+370", "phoneDigits": 8, "phoneMask": "### #####", "phoneGroupSizes": [3, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 442, "alpha2": "lu", "alpha3": "lux", "name": "Luxembourg", "phoneCode": "+352", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 450, "alpha2": "mg", "alpha3": "mdg", "name": "Madagascar", "phoneCode": "+261", "phoneDigits": 9, "phoneMask": "## ## ### ##", "phoneGroupSizes": [2, 2, 3, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 454, "alpha2": "mw", "alpha3": "mwi", "name": "Malawi", "phoneCode": "+265", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 458, "alpha2": "my", "alpha3": "mys", "name": "Malaysia", "phoneCode": "+60", "phoneDigits": 10, "phoneMask": "##-#### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 462, "alpha2": "mv", "alpha3": "mdv", "name": "Maldives", "phoneCode": "+960", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 466, "alpha2": "ml", "alpha3": "mli", "name": "Mali", "phoneCode": "+223", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 470, "alpha2": "mt", "alpha3": "mlt", "name": "Malta", "phoneCode": "+356", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 584, "alpha2": "mh", "alpha3": "mhl", "name": "Marshall Islands", "phoneCode": "+692", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 478, "alpha2": "mr", "alpha3": "mrt", "name": "Mauritania", "phoneCode": "+222", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 480, "alpha2": "mu", "alpha3": "mus", "name": "Mauritius", "phoneCode": "+230", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 484, "alpha2": "mx", "alpha3": "mex", "name": "Mexico", "phoneCode": "+52", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 583, "alpha2": "fm", "alpha3": "fsm", "name": "Micronesia, Federated States of", "phoneCode": "+691", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 498, "alpha2": "md", "alpha3": "mda", "name": "Moldova, Republic of", "phoneCode": "+373", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 492, "alpha2": "mc", "alpha3": "mco", "name": "Monaco", "phoneCode": "+377", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 496, "alpha2": "mn", "alpha3": "mng", "name": "Mongolia", "phoneCode": "+976", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 499, "alpha2": "me", "alpha3": "mne", "name": "Montenegro", "phoneCode": "+382", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 504, "alpha2": "ma", "alpha3": "mar", "name": "Morocco", "phoneCode": "+212", "phoneDigits": 9, "phoneMask": "###-###-###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 508, "alpha2": "mz", "alpha3": "moz", "name": "Mozambique", "phoneCode": "+258", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 104, "alpha2": "mm", "alpha3": "mmr", "name": "Myanmar", "phoneCode": "+95", "phoneDigits": 9, "phoneMask": "# ### ####", "phoneGroupSizes": [1, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 516, "alpha2": "na", "alpha3": "nam", "name": "Namibia", "phoneCode": "+264", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 520, "alpha2": "nr", "alpha3": "nru", "name": "Nauru", "phoneCode": "+674", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 524, "alpha2": "np", "alpha3": "npl", "name": "Nepal", "phoneCode": "+977", "phoneDigits": 10, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 528, "alpha2": "nl", "alpha3": "nld", "name": "Netherlands", "phoneCode": "+31", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 554, "alpha2": "nz", "alpha3": "nzl", "name": "New Zealand", "phoneCode": "+64", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 558, "alpha2": "ni", "alpha3": "nic", "name": "Nicaragua", "phoneCode": "+505", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 562, "alpha2": "ne", "alpha3": "ner", "name": "Niger", "phoneCode": "+227", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 566, "alpha2": "ng", "alpha3": "nga", "name": "Nigeria", "phoneCode": "+234", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 807, "alpha2": "mk", "alpha3": "mkd", "name": "North Macedonia", "phoneCode": "+389", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 578, "alpha2": "no", "alpha3": "nor", "name": "Norway", "phoneCode": "+47", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 512, "alpha2": "om", "alpha3": "omn", "name": "Oman", "phoneCode": "+968", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 586, "alpha2": "pk", "alpha3": "pak", "name": "Pakistan", "phoneCode": "+92", "phoneDigits": 10, "phoneMask": "### #######", "phoneGroupSizes": [3, 7], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 585, "alpha2": "pw", "alpha3": "plw", "name": "Palau", "phoneCode": "+680", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 591, "alpha2": "pa", "alpha3": "pan", "name": "Panama", "phoneCode": "+507", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 598, "alpha2": "pg", "alpha3": "png", "name": "Papua New Guinea", "phoneCode": "+675", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 600, "alpha2": "py", "alpha3": "pry", "name": "Paraguay", "phoneCode": "+595", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 604, "alpha2": "pe", "alpha3": "per", "name": "Peru", "phoneCode": "+51", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 608, "alpha2": "ph", "alpha3": "phl", "name": "Philippines", "phoneCode": "+63", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 616, "alpha2": "pl", "alpha3": "pol", "name": "Poland", "phoneCode": "+48", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 620, "alpha2": "pt", "alpha3": "prt", "name": "Portugal", "phoneCode": "+351", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 634, "alpha2": "qa", "alpha3": "qat", "name": "Qatar", "phoneCode": "+974", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 642, "alpha2": "ro", "alpha3": "rou", "name": "Romania", "phoneCode": "+40", "phoneDigits": 10, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 643, "alpha2": "ru", "alpha3": "rus", "name": "Russian Federation", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]}, +{"id": 646, "alpha2": "rw", "alpha3": "rwa", "name": "Rwanda", "phoneCode": "+250", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 659, "alpha2": "kn", "alpha3": "kna", "name": "Saint Kitts and Nevis", "phoneCode": "+1869", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 662, "alpha2": "lc", "alpha3": "lca", "name": "Saint Lucia", "phoneCode": "+1758", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 670, "alpha2": "vc", "alpha3": "vct", "name": "Saint Vincent and the Grenadines", "phoneCode": "+1784", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 882, "alpha2": "ws", "alpha3": "wsm", "name": "Samoa", "phoneCode": "+685", "phoneDigits": 7, "phoneMask": "## #####", "phoneGroupSizes": [2, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 674, "alpha2": "sm", "alpha3": "smr", "name": "San Marino", "phoneCode": "+378", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 678, "alpha2": "st", "alpha3": "stp", "name": "Sao Tome and Principe", "phoneCode": "+239", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 682, "alpha2": "sa", "alpha3": "sau", "name": "Saudi Arabia", "phoneCode": "+966", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 686, "alpha2": "sn", "alpha3": "sen", "name": "Senegal", "phoneCode": "+221", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 688, "alpha2": "rs", "alpha3": "srb", "name": "Serbia", "phoneCode": "+381", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 690, "alpha2": "sc", "alpha3": "syc", "name": "Seychelles", "phoneCode": "+248", "phoneDigits": 7, "phoneMask": "# ### ###", "phoneGroupSizes": [1, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 694, "alpha2": "sl", "alpha3": "sle", "name": "Sierra Leone", "phoneCode": "+232", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 702, "alpha2": "sg", "alpha3": "sgp", "name": "Singapore", "phoneCode": "+65", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 703, "alpha2": "sk", "alpha3": "svk", "name": "Slovakia", "phoneCode": "+421", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 705, "alpha2": "si", "alpha3": "svn", "name": "Slovenia", "phoneCode": "+386", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 90, "alpha2": "sb", "alpha3": "slb", "name": "Solomon Islands", "phoneCode": "+677", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 706, "alpha2": "so", "alpha3": "som", "name": "Somalia", "phoneCode": "+252", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 710, "alpha2": "za", "alpha3": "zaf", "name": "South Africa", "phoneCode": "+27", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 728, "alpha2": "ss", "alpha3": "ssd", "name": "South Sudan", "phoneCode": "+211", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 724, "alpha2": "es", "alpha3": "esp", "name": "Spain", "phoneCode": "+34", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 144, "alpha2": "lk", "alpha3": "lka", "name": "Sri Lanka", "phoneCode": "+94", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 729, "alpha2": "sd", "alpha3": "sdn", "name": "Sudan", "phoneCode": "+249", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 740, "alpha2": "sr", "alpha3": "sur", "name": "Suriname", "phoneCode": "+597", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 752, "alpha2": "se", "alpha3": "swe", "name": "Sweden", "phoneCode": "+46", "phoneDigits": 9, "phoneMask": "##-### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", "-", " ", " ", ""]}, +{"id": 756, "alpha2": "ch", "alpha3": "che", "name": "Switzerland", "phoneCode": "+41", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 760, "alpha2": "sy", "alpha3": "syr", "name": "Syrian Arab Republic", "phoneCode": "+963", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 762, "alpha2": "tj", "alpha3": "tjk", "name": "Tajikistan", "phoneCode": "+992", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 834, "alpha2": "tz", "alpha3": "tza", "name": "Tanzania, United Republic of", "phoneCode": "+255", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 764, "alpha2": "th", "alpha3": "tha", "name": "Thailand", "phoneCode": "+66", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 626, "alpha2": "tl", "alpha3": "tls", "name": "Timor-Leste", "phoneCode": "+670", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 768, "alpha2": "tg", "alpha3": "tgo", "name": "Togo", "phoneCode": "+228", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 776, "alpha2": "to", "alpha3": "ton", "name": "Tonga", "phoneCode": "+676", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 780, "alpha2": "tt", "alpha3": "tto", "name": "Trinidad and Tobago", "phoneCode": "+1868", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 788, "alpha2": "tn", "alpha3": "tun", "name": "Tunisia", "phoneCode": "+216", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 792, "alpha2": "tr", "alpha3": "tur", "name": "Türkiye", "phoneCode": "+90", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 795, "alpha2": "tm", "alpha3": "tkm", "name": "Turkmenistan", "phoneCode": "+993", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 798, "alpha2": "tv", "alpha3": "tuv", "name": "Tuvalu", "phoneCode": "+688", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]}, +{"id": 800, "alpha2": "ug", "alpha3": "uga", "name": "Uganda", "phoneCode": "+256", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 804, "alpha2": "ua", "alpha3": "ukr", "name": "Ukraine", "phoneCode": "+380", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 784, "alpha2": "ae", "alpha3": "are", "name": "United Arab Emirates", "phoneCode": "+971", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 826, "alpha2": "gb", "alpha3": "gbr", "name": "United Kingdom of Great Britain and Northern Ireland", "phoneCode": "+44", "phoneDigits": 10, "phoneMask": "#### ######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 840, "alpha2": "us", "alpha3": "usa", "name": "United States of America", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 858, "alpha2": "uy", "alpha3": "ury", "name": "Uruguay", "phoneCode": "+598", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 860, "alpha2": "uz", "alpha3": "uzb", "name": "Uzbekistan", "phoneCode": "+998", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 548, "alpha2": "vu", "alpha3": "vut", "name": "Vanuatu", "phoneCode": "+678", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 862, "alpha2": "ve", "alpha3": "ven", "name": "Venezuela, Bolivarian Republic of", "phoneCode": "+58", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 704, "alpha2": "vn", "alpha3": "vnm", "name": "Viet Nam", "phoneCode": "+84", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 887, "alpha2": "ye", "alpha3": "yem", "name": "Yemen", "phoneCode": "+967", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 894, "alpha2": "zm", "alpha3": "zmb", "name": "Zambia", "phoneCode": "+260", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 716, "alpha2": "zw", "alpha3": "zwe", "name": "Zimbabwe", "phoneCode": "+263", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +]'''; + +const String _countriesRuJson = '''[ +{"id": 36, "alpha2": "au", "alpha3": "aus", "name": "Австралия", "phoneCode": "+61", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 40, "alpha2": "at", "alpha3": "aut", "name": "Австрия", "phoneCode": "+43", "phoneDigits": 10, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 31, "alpha2": "az", "alpha3": "aze", "name": "Азербайджан", "phoneCode": "+994", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 8, "alpha2": "al", "alpha3": "alb", "name": "Албания", "phoneCode": "+355", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 12, "alpha2": "dz", "alpha3": "dza", "name": "Алжир", "phoneCode": "+213", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 24, "alpha2": "ao", "alpha3": "ago", "name": "Ангола", "phoneCode": "+244", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 20, "alpha2": "ad", "alpha3": "and", "name": "Андорра", "phoneCode": "+376", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]}, +{"id": 28, "alpha2": "ag", "alpha3": "atg", "name": "Антигуа и Барбуда", "phoneCode": "+1268", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 32, "alpha2": "ar", "alpha3": "arg", "name": "Аргентина", "phoneCode": "+54", "phoneDigits": 10, "phoneMask": "(##) ####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 51, "alpha2": "am", "alpha3": "arm", "name": "Армения", "phoneCode": "+374", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 4, "alpha2": "af", "alpha3": "afg", "name": "Афганистан", "phoneCode": "+93", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 44, "alpha2": "bs", "alpha3": "bhs", "name": "Багамские Острова", "phoneCode": "+1242", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 50, "alpha2": "bd", "alpha3": "bgd", "name": "Бангладеш", "phoneCode": "+880", "phoneDigits": 10, "phoneMask": "####-######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 52, "alpha2": "bb", "alpha3": "brb", "name": "Барбадос", "phoneCode": "+1246", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 48, "alpha2": "bh", "alpha3": "bhr", "name": "Бахрейн", "phoneCode": "+973", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 84, "alpha2": "bz", "alpha3": "blz", "name": "Белиз", "phoneCode": "+501", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 112, "alpha2": "by", "alpha3": "blr", "name": "Беларусь", "phoneCode": "+375", "phoneDigits": 9, "phoneMask": "## ###-##-##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", "-", "-", ""]}, +{"id": 56, "alpha2": "be", "alpha3": "bel", "name": "Бельгия", "phoneCode": "+32", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 204, "alpha2": "bj", "alpha3": "ben", "name": "Бенин", "phoneCode": "+229", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 100, "alpha2": "bg", "alpha3": "bgr", "name": "Болгария", "phoneCode": "+359", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 68, "alpha2": "bo", "alpha3": "bol", "name": "Боливия", "phoneCode": "+591", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]}, +{"id": 70, "alpha2": "ba", "alpha3": "bih", "name": "Босния и Герцеговина", "phoneCode": "+387", "phoneDigits": 8, "phoneMask": "## ###-###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", "-", ""]}, +{"id": 72, "alpha2": "bw", "alpha3": "bwa", "name": "Ботсвана", "phoneCode": "+267", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 76, "alpha2": "br", "alpha3": "bra", "name": "Бразилия", "phoneCode": "+55", "phoneDigits": 11, "phoneMask": "(##) #####-####", "phoneGroupSizes": [2, 5, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 96, "alpha2": "bn", "alpha3": "brn", "name": "Бруней", "phoneCode": "+673", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 854, "alpha2": "bf", "alpha3": "bfa", "name": "Буркина-Фасо", "phoneCode": "+226", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 108, "alpha2": "bi", "alpha3": "bdi", "name": "Бурунди", "phoneCode": "+257", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 64, "alpha2": "bt", "alpha3": "btn", "name": "Бутан", "phoneCode": "+975", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 548, "alpha2": "vu", "alpha3": "vut", "name": "Вануату", "phoneCode": "+678", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 826, "alpha2": "gb", "alpha3": "gbr", "name": "Великобритания", "phoneCode": "+44", "phoneDigits": 10, "phoneMask": "#### ######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 348, "alpha2": "hu", "alpha3": "hun", "name": "Венгрия", "phoneCode": "+36", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 862, "alpha2": "ve", "alpha3": "ven", "name": "Венесуэла", "phoneCode": "+58", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 626, "alpha2": "tl", "alpha3": "tls", "name": "Восточный Тимор", "phoneCode": "+670", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 704, "alpha2": "vn", "alpha3": "vnm", "name": "Вьетнам", "phoneCode": "+84", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 266, "alpha2": "ga", "alpha3": "gab", "name": "Габон", "phoneCode": "+241", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 332, "alpha2": "ht", "alpha3": "hti", "name": "Гаити", "phoneCode": "+509", "phoneDigits": 8, "phoneMask": "## ## ####", "phoneGroupSizes": [2, 2, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 328, "alpha2": "gy", "alpha3": "guy", "name": "Гайана", "phoneCode": "+592", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 270, "alpha2": "gm", "alpha3": "gmb", "name": "Гамбия", "phoneCode": "+220", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 288, "alpha2": "gh", "alpha3": "gha", "name": "Гана", "phoneCode": "+233", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 320, "alpha2": "gt", "alpha3": "gtm", "name": "Гватемала", "phoneCode": "+502", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 324, "alpha2": "gn", "alpha3": "gin", "name": "Гвинея", "phoneCode": "+224", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 624, "alpha2": "gw", "alpha3": "gnb", "name": "Гвинея-Бисау", "phoneCode": "+245", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 276, "alpha2": "de", "alpha3": "deu", "name": "Германия", "phoneCode": "+49", "phoneDigits": 10, "phoneMask": "#### #######", "phoneGroupSizes": [4, 7], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 340, "alpha2": "hn", "alpha3": "hnd", "name": "Гондурас", "phoneCode": "+504", "phoneDigits": 8, "phoneMask": "####-####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 308, "alpha2": "gd", "alpha3": "grd", "name": "Гренада", "phoneCode": "+1473", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 300, "alpha2": "gr", "alpha3": "grc", "name": "Греция", "phoneCode": "+30", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 268, "alpha2": "ge", "alpha3": "geo", "name": "Грузия", "phoneCode": "+995", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 208, "alpha2": "dk", "alpha3": "dnk", "name": "Дания", "phoneCode": "+45", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 262, "alpha2": "dj", "alpha3": "dji", "name": "Джибути", "phoneCode": "+253", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 212, "alpha2": "dm", "alpha3": "dma", "name": "Доминика", "phoneCode": "+1767", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 214, "alpha2": "do", "alpha3": "dom", "name": "Доминиканская Республика", "phoneCode": "+1809", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 180, "alpha2": "cd", "alpha3": "cod", "name": "ДР Конго", "phoneCode": "+243", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 818, "alpha2": "eg", "alpha3": "egy", "name": "Египет", "phoneCode": "+20", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 894, "alpha2": "zm", "alpha3": "zmb", "name": "Замбия", "phoneCode": "+260", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 716, "alpha2": "zw", "alpha3": "zwe", "name": "Зимбабве", "phoneCode": "+263", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 376, "alpha2": "il", "alpha3": "isr", "name": "Израиль", "phoneCode": "+972", "phoneDigits": 9, "phoneMask": "##-###-####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 356, "alpha2": "in", "alpha3": "ind", "name": "Индия", "phoneCode": "+91", "phoneDigits": 10, "phoneMask": "##### #####", "phoneGroupSizes": [5, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 360, "alpha2": "id", "alpha3": "idn", "name": "Индонезия", "phoneCode": "+62", "phoneDigits": 10, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 400, "alpha2": "jo", "alpha3": "jor", "name": "Иордания", "phoneCode": "+962", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 368, "alpha2": "iq", "alpha3": "irq", "name": "Ирак", "phoneCode": "+964", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 364, "alpha2": "ir", "alpha3": "irn", "name": "Иран", "phoneCode": "+98", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 372, "alpha2": "ie", "alpha3": "irl", "name": "Ирландия", "phoneCode": "+353", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 352, "alpha2": "is", "alpha3": "isl", "name": "Исландия", "phoneCode": "+354", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 724, "alpha2": "es", "alpha3": "esp", "name": "Испания", "phoneCode": "+34", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 380, "alpha2": "it", "alpha3": "ita", "name": "Италия", "phoneCode": "+39", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 887, "alpha2": "ye", "alpha3": "yem", "name": "Йемен", "phoneCode": "+967", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 132, "alpha2": "cv", "alpha3": "cpv", "name": "Кабо-Верде", "phoneCode": "+238", "phoneDigits": 7, "phoneMask": "### ## ##", "phoneGroupSizes": [3, 2, 2], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 398, "alpha2": "kz", "alpha3": "kaz", "name": "Казахстан", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]}, +{"id": 116, "alpha2": "kh", "alpha3": "khm", "name": "Камбоджа", "phoneCode": "+855", "phoneDigits": 9, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 120, "alpha2": "cm", "alpha3": "cmr", "name": "Камерун", "phoneCode": "+237", "phoneDigits": 9, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 124, "alpha2": "ca", "alpha3": "can", "name": "Канада", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 634, "alpha2": "qa", "alpha3": "qat", "name": "Катар", "phoneCode": "+974", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 404, "alpha2": "ke", "alpha3": "ken", "name": "Кения", "phoneCode": "+254", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 196, "alpha2": "cy", "alpha3": "cyp", "name": "Кипр", "phoneCode": "+357", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 417, "alpha2": "kg", "alpha3": "kgz", "name": "Кыргызстан", "phoneCode": "+996", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 296, "alpha2": "ki", "alpha3": "kir", "name": "Кирибати", "phoneCode": "+686", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 408, "alpha2": "kp", "alpha3": "prk", "name": "КНДР (Корейская Народно-Демократическая Республика)", "phoneCode": "+850", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 156, "alpha2": "cn", "alpha3": "chn", "name": "Китай (Китайская Народная Республика)", "phoneCode": "+86", "phoneDigits": 11, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 170, "alpha2": "co", "alpha3": "col", "name": "Колумбия", "phoneCode": "+57", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 174, "alpha2": "km", "alpha3": "com", "name": "Коморы", "phoneCode": "+269", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 188, "alpha2": "cr", "alpha3": "cri", "name": "Коста-Рика", "phoneCode": "+506", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 384, "alpha2": "ci", "alpha3": "civ", "name": "Кот-д’Ивуар", "phoneCode": "+225", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 192, "alpha2": "cu", "alpha3": "cub", "name": "Куба", "phoneCode": "+53", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]}, +{"id": 414, "alpha2": "kw", "alpha3": "kwt", "name": "Кувейт", "phoneCode": "+965", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 418, "alpha2": "la", "alpha3": "lao", "name": "Лаос", "phoneCode": "+856", "phoneDigits": 9, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 428, "alpha2": "lv", "alpha3": "lva", "name": "Латвия", "phoneCode": "+371", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 426, "alpha2": "ls", "alpha3": "lso", "name": "Лесото", "phoneCode": "+266", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 430, "alpha2": "lr", "alpha3": "lbr", "name": "Либерия", "phoneCode": "+231", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 422, "alpha2": "lb", "alpha3": "lbn", "name": "Ливан", "phoneCode": "+961", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 434, "alpha2": "ly", "alpha3": "lby", "name": "Ливия", "phoneCode": "+218", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 440, "alpha2": "lt", "alpha3": "ltu", "name": "Литва", "phoneCode": "+370", "phoneDigits": 8, "phoneMask": "### #####", "phoneGroupSizes": [3, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 438, "alpha2": "li", "alpha3": "lie", "name": "Лихтенштейн", "phoneCode": "+423", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 442, "alpha2": "lu", "alpha3": "lux", "name": "Люксембург", "phoneCode": "+352", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 480, "alpha2": "mu", "alpha3": "mus", "name": "Маврикий", "phoneCode": "+230", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 478, "alpha2": "mr", "alpha3": "mrt", "name": "Мавритания", "phoneCode": "+222", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 450, "alpha2": "mg", "alpha3": "mdg", "name": "Мадагаскар", "phoneCode": "+261", "phoneDigits": 9, "phoneMask": "## ## ### ##", "phoneGroupSizes": [2, 2, 3, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 807, "alpha2": "mk", "alpha3": "mkd", "name": "Северная Македония", "phoneCode": "+389", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 454, "alpha2": "mw", "alpha3": "mwi", "name": "Малави", "phoneCode": "+265", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 458, "alpha2": "my", "alpha3": "mys", "name": "Малайзия", "phoneCode": "+60", "phoneDigits": 10, "phoneMask": "##-#### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 466, "alpha2": "ml", "alpha3": "mli", "name": "Мали", "phoneCode": "+223", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 462, "alpha2": "mv", "alpha3": "mdv", "name": "Мальдивы", "phoneCode": "+960", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 470, "alpha2": "mt", "alpha3": "mlt", "name": "Мальта", "phoneCode": "+356", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 504, "alpha2": "ma", "alpha3": "mar", "name": "Марокко", "phoneCode": "+212", "phoneDigits": 9, "phoneMask": "###-###-###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 584, "alpha2": "mh", "alpha3": "mhl", "name": "Маршалловы Острова", "phoneCode": "+692", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 484, "alpha2": "mx", "alpha3": "mex", "name": "Мексика", "phoneCode": "+52", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 583, "alpha2": "fm", "alpha3": "fsm", "name": "Микронезия", "phoneCode": "+691", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 508, "alpha2": "mz", "alpha3": "moz", "name": "Мозамбик", "phoneCode": "+258", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 498, "alpha2": "md", "alpha3": "mda", "name": "Молдова", "phoneCode": "+373", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 492, "alpha2": "mc", "alpha3": "mco", "name": "Монако", "phoneCode": "+377", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 496, "alpha2": "mn", "alpha3": "mng", "name": "Монголия", "phoneCode": "+976", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 104, "alpha2": "mm", "alpha3": "mmr", "name": "Мьянма", "phoneCode": "+95", "phoneDigits": 9, "phoneMask": "# ### ####", "phoneGroupSizes": [1, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 516, "alpha2": "na", "alpha3": "nam", "name": "Намибия", "phoneCode": "+264", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 520, "alpha2": "nr", "alpha3": "nru", "name": "Науру", "phoneCode": "+674", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 524, "alpha2": "np", "alpha3": "npl", "name": "Непал", "phoneCode": "+977", "phoneDigits": 10, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 562, "alpha2": "ne", "alpha3": "ner", "name": "Нигер", "phoneCode": "+227", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 566, "alpha2": "ng", "alpha3": "nga", "name": "Нигерия", "phoneCode": "+234", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 528, "alpha2": "nl", "alpha3": "nld", "name": "Нидерланды", "phoneCode": "+31", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 558, "alpha2": "ni", "alpha3": "nic", "name": "Никарагуа", "phoneCode": "+505", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 554, "alpha2": "nz", "alpha3": "nzl", "name": "Новая Зеландия", "phoneCode": "+64", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 578, "alpha2": "no", "alpha3": "nor", "name": "Норвегия", "phoneCode": "+47", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 784, "alpha2": "ae", "alpha3": "are", "name": "ОАЭ", "phoneCode": "+971", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 512, "alpha2": "om", "alpha3": "omn", "name": "Оман", "phoneCode": "+968", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 586, "alpha2": "pk", "alpha3": "pak", "name": "Пакистан", "phoneCode": "+92", "phoneDigits": 10, "phoneMask": "### #######", "phoneGroupSizes": [3, 7], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 585, "alpha2": "pw", "alpha3": "plw", "name": "Палау", "phoneCode": "+680", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 591, "alpha2": "pa", "alpha3": "pan", "name": "Панама", "phoneCode": "+507", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 598, "alpha2": "pg", "alpha3": "png", "name": "Папуа — Новая Гвинея", "phoneCode": "+675", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 600, "alpha2": "py", "alpha3": "pry", "name": "Парагвай", "phoneCode": "+595", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 604, "alpha2": "pe", "alpha3": "per", "name": "Перу", "phoneCode": "+51", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 616, "alpha2": "pl", "alpha3": "pol", "name": "Польша", "phoneCode": "+48", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 620, "alpha2": "pt", "alpha3": "prt", "name": "Португалия", "phoneCode": "+351", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 178, "alpha2": "cg", "alpha3": "cog", "name": "Республика Конго", "phoneCode": "+242", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 410, "alpha2": "kr", "alpha3": "kor", "name": "Республика Корея", "phoneCode": "+82", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]}, +{"id": 643, "alpha2": "ru", "alpha3": "rus", "name": "Россия", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]}, +{"id": 646, "alpha2": "rw", "alpha3": "rwa", "name": "Руанда", "phoneCode": "+250", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 642, "alpha2": "ro", "alpha3": "rou", "name": "Румыния", "phoneCode": "+40", "phoneDigits": 10, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 222, "alpha2": "sv", "alpha3": "slv", "name": "Сальвадор", "phoneCode": "+503", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 882, "alpha2": "ws", "alpha3": "wsm", "name": "Самоа", "phoneCode": "+685", "phoneDigits": 7, "phoneMask": "## #####", "phoneGroupSizes": [2, 5], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 674, "alpha2": "sm", "alpha3": "smr", "name": "Сан-Марино", "phoneCode": "+378", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 678, "alpha2": "st", "alpha3": "stp", "name": "Сан-Томе и Принсипи", "phoneCode": "+239", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 682, "alpha2": "sa", "alpha3": "sau", "name": "Саудовская Аравия", "phoneCode": "+966", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 690, "alpha2": "sc", "alpha3": "syc", "name": "Сейшельские Острова", "phoneCode": "+248", "phoneDigits": 7, "phoneMask": "# ### ###", "phoneGroupSizes": [1, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 686, "alpha2": "sn", "alpha3": "sen", "name": "Сенегал", "phoneCode": "+221", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 670, "alpha2": "vc", "alpha3": "vct", "name": "Сент-Винсент и Гренадины", "phoneCode": "+1784", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 659, "alpha2": "kn", "alpha3": "kna", "name": "Сент-Китс и Невис", "phoneCode": "+1869", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 662, "alpha2": "lc", "alpha3": "lca", "name": "Сент-Люсия", "phoneCode": "+1758", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 688, "alpha2": "rs", "alpha3": "srb", "name": "Сербия", "phoneCode": "+381", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 702, "alpha2": "sg", "alpha3": "sgp", "name": "Сингапур", "phoneCode": "+65", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 760, "alpha2": "sy", "alpha3": "syr", "name": "Сирия", "phoneCode": "+963", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 703, "alpha2": "sk", "alpha3": "svk", "name": "Словакия", "phoneCode": "+421", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 705, "alpha2": "si", "alpha3": "svn", "name": "Словения", "phoneCode": "+386", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 90, "alpha2": "sb", "alpha3": "slb", "name": "Соломоновы Острова", "phoneCode": "+677", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 706, "alpha2": "so", "alpha3": "som", "name": "Сомали", "phoneCode": "+252", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 729, "alpha2": "sd", "alpha3": "sdn", "name": "Судан", "phoneCode": "+249", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 740, "alpha2": "sr", "alpha3": "sur", "name": "Суринам", "phoneCode": "+597", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]}, +{"id": 840, "alpha2": "us", "alpha3": "usa", "name": "США", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 694, "alpha2": "sl", "alpha3": "sle", "name": "Сьерра-Леоне", "phoneCode": "+232", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 762, "alpha2": "tj", "alpha3": "tjk", "name": "Таджикистан", "phoneCode": "+992", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 764, "alpha2": "th", "alpha3": "tha", "name": "Таиланд", "phoneCode": "+66", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]}, +{"id": 834, "alpha2": "tz", "alpha3": "tza", "name": "Танзания", "phoneCode": "+255", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 768, "alpha2": "tg", "alpha3": "tgo", "name": "Того", "phoneCode": "+228", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 776, "alpha2": "to", "alpha3": "ton", "name": "Тонга", "phoneCode": "+676", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 780, "alpha2": "tt", "alpha3": "tto", "name": "Тринидад и Тобаго", "phoneCode": "+1868", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 798, "alpha2": "tv", "alpha3": "tuv", "name": "Тувалу", "phoneCode": "+688", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]}, +{"id": 788, "alpha2": "tn", "alpha3": "tun", "name": "Тунис", "phoneCode": "+216", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 795, "alpha2": "tm", "alpha3": "tkm", "name": "Туркменистан", "phoneCode": "+993", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 792, "alpha2": "tr", "alpha3": "tur", "name": "Турция", "phoneCode": "+90", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 800, "alpha2": "ug", "alpha3": "uga", "name": "Уганда", "phoneCode": "+256", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 860, "alpha2": "uz", "alpha3": "uzb", "name": "Узбекистан", "phoneCode": "+998", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 804, "alpha2": "ua", "alpha3": "ukr", "name": "Украина", "phoneCode": "+380", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 858, "alpha2": "uy", "alpha3": "ury", "name": "Уругвай", "phoneCode": "+598", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 242, "alpha2": "fj", "alpha3": "fji", "name": "Фиджи", "phoneCode": "+679", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 608, "alpha2": "ph", "alpha3": "phl", "name": "Филиппины", "phoneCode": "+63", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 246, "alpha2": "fi", "alpha3": "fin", "name": "Финляндия", "phoneCode": "+358", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 250, "alpha2": "fr", "alpha3": "fra", "name": "Франция", "phoneCode": "+33", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]}, +{"id": 191, "alpha2": "hr", "alpha3": "hrv", "name": "Хорватия", "phoneCode": "+385", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 140, "alpha2": "cf", "alpha3": "caf", "name": "ЦАР", "phoneCode": "+236", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 148, "alpha2": "td", "alpha3": "tcd", "name": "Чад", "phoneCode": "+235", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 499, "alpha2": "me", "alpha3": "mne", "name": "Черногория", "phoneCode": "+382", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 203, "alpha2": "cz", "alpha3": "cze", "name": "Чехия", "phoneCode": "+420", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 152, "alpha2": "cl", "alpha3": "chl", "name": "Чили", "phoneCode": "+56", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 756, "alpha2": "ch", "alpha3": "che", "name": "Швейцария", "phoneCode": "+41", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 752, "alpha2": "se", "alpha3": "swe", "name": "Швеция", "phoneCode": "+46", "phoneDigits": 9, "phoneMask": "##-### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", "-", " ", " ", ""]}, +{"id": 144, "alpha2": "lk", "alpha3": "lka", "name": "Шри-Ланка", "phoneCode": "+94", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 218, "alpha2": "ec", "alpha3": "ecu", "name": "Эквадор", "phoneCode": "+593", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 226, "alpha2": "gq", "alpha3": "gnq", "name": "Экваториальная Гвинея", "phoneCode": "+240", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 232, "alpha2": "er", "alpha3": "eri", "name": "Эритрея", "phoneCode": "+291", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 748, "alpha2": "sz", "alpha3": "swz", "name": "Эсватини", "phoneCode": "+268", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]}, +{"id": 233, "alpha2": "ee", "alpha3": "est", "name": "Эстония", "phoneCode": "+372", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]}, +{"id": 231, "alpha2": "et", "alpha3": "eth", "name": "Эфиопия", "phoneCode": "+251", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 710, "alpha2": "za", "alpha3": "zaf", "name": "ЮАР", "phoneCode": "+27", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 728, "alpha2": "ss", "alpha3": "ssd", "name": "Южный Судан", "phoneCode": "+211", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]}, +{"id": 388, "alpha2": "jm", "alpha3": "jam", "name": "Ямайка", "phoneCode": "+1876", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]}, +{"id": 392, "alpha2": "jp", "alpha3": "jpn", "name": "Япония", "phoneCode": "+81", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +]'''; \ No newline at end of file diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 39b828b..cd9861f 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:material_symbols_icons/symbols.dart'; import '../chats/chat_list_screen.dart'; class CodeConfirmationScreen extends StatefulWidget { diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index 700742c..d950724 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:komet/core/config/countries.dart'; import 'code_confirmation_screen.dart'; class LoginScreen extends StatefulWidget { @@ -14,8 +15,89 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final TextEditingController _phoneController = TextEditingController(); + late CountryName _selectedCountry; bool _isPhoneValid = false; + @override + void initState() { + super.initState(); + _selectedCountry = countriesByCode['RU'] ?? allCountries.first; + } + + void _showCountryPicker() { + showModalBottomSheet( + context: context, + backgroundColor: const Color(0xFF1E1E2A), + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 1.0, + snap: true, + snapSizes: const [0.7, 1.0], + expand: false, + builder: (context, scrollController) => Column( + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Выберите страну', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Symbols.close, color: Colors.white70), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: allCountries.length, + itemBuilder: (context, index) { + final country = allCountries[index]; + return ListTile( + leading: Text( + country.phoneCode, + style: const TextStyle(color: Colors.white54, fontSize: 14), + ), + title: Text( + country.ru, + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + onTap: () { + setState(() { + _selectedCountry = country; + _phoneController.clear(); + _isPhoneValid = false; + }); + Navigator.pop(context); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } + void _showTOS(BuildContext context) { showModalBottomSheet( context: context, @@ -205,8 +287,8 @@ class _LoginScreenState extends State { ), const SizedBox(height: 12), Text( - '+7 $formattedPhone', - style: const TextStyle( + '${_selectedCountry.phoneCode} $formattedPhone', + style: GoogleFonts.inter( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500, @@ -220,10 +302,10 @@ class _LoginScreenState extends State { children: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text( + child: Text( 'Изменить', - style: TextStyle( - color: Color(0xFFAFAFFF), + style: GoogleFonts.inter( + color: const Color(0xFFAFAFFF), fontSize: 15, fontWeight: FontWeight.w500, ), @@ -236,15 +318,15 @@ class _LoginScreenState extends State { context, MaterialPageRoute( builder: (context) => CodeConfirmationScreen( - phoneNumber: '+7 $formattedPhone', + phoneNumber: '${_selectedCountry.phoneCode} $formattedPhone', ), ), ); }, - child: const Text( + child: Text( 'Готово', - style: TextStyle( - color: Color(0xFFAFAFFF), + style: GoogleFonts.inter( + color: const Color(0xFFAFAFFF), fontSize: 15, fontWeight: FontWeight.w600, ), @@ -288,7 +370,7 @@ class _LoginScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 16), + const SizedBox(height: 44), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -310,7 +392,7 @@ class _LoginScreenState extends State { ), ], ), - const SizedBox(height: 24), + const SizedBox(height: 74), Center( child: Column( children: [ @@ -324,7 +406,7 @@ class _LoginScreenState extends State { 'Войдите в Komet', style: GoogleFonts.inter( color: Colors.white, - fontSize: 20, + fontSize: 32, fontWeight: FontWeight.w500, ), ), @@ -334,7 +416,7 @@ class _LoginScreenState extends State { textAlign: TextAlign.center, style: TextStyle( color: Colors.white70, - fontSize: 13, + fontSize: 16, fontWeight: FontWeight.w400, height: 1.4, ), @@ -342,29 +424,41 @@ class _LoginScreenState extends State { ], ), ), - const SizedBox(height: 24), - _buildInputField( - label: 'Страна', - content: Row( - children: [ - const Text( - 'Россия', - style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), - ), - const Spacer(), - const Icon(Icons.keyboard_arrow_down, color: Colors.white70), - ], + const SizedBox(height: 48), + InkWell( + onTap: _showCountryPicker, + borderRadius: BorderRadius.circular(50), + child: _buildInputField( + label: 'Страна', + content: Row( + children: [ + Text( + _selectedCountry.ru, + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + ), + const Spacer(), + const Icon(Icons.keyboard_arrow_down, color: Colors.white70), + ], + ), ), ), - const SizedBox(height: 12), + const SizedBox(height: 40), _buildInputField( label: 'Номер телефона', content: Row( children: [ - const Text( - '+7', - style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), - ), + Text( + _selectedCountry.phoneCode, + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + ), const SizedBox(width: 12), Container( width: 1, @@ -378,7 +472,7 @@ class _LoginScreenState extends State { keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _PhoneInputFormatter(), + _PhoneInputFormatter(_selectedCountry), ], style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), decoration: const InputDecoration( @@ -391,7 +485,7 @@ class _LoginScreenState extends State { onChanged: (value) { final digits = value.replaceAll(RegExp(r'\D'), ''); setState(() { - _isPhoneValid = digits.length == 10; + _isPhoneValid = digits.length == _selectedCountry.phoneDigits; }); }, ), @@ -423,7 +517,7 @@ class _LoginScreenState extends State { text: TextSpan( style: GoogleFonts.inter( color: Colors.white, - fontSize: 11, + fontSize: 14, fontWeight: FontWeight.w500, height: 1.4, ), @@ -512,8 +606,8 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( label, - style: const TextStyle( - color: Color(0xFFBEC2FF), + style: GoogleFonts.inter( + color: const Color(0xFFBEC2FF), fontSize: 14, fontWeight: FontWeight.w400, ), @@ -528,6 +622,9 @@ class _LoginScreenState extends State { } class _PhoneInputFormatter extends TextInputFormatter { + final CountryName country; + _PhoneInputFormatter(this.country); + @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { @@ -540,14 +637,33 @@ class _PhoneInputFormatter extends TextInputFormatter { } } - if (text.length > 10) text = text.substring(0, 10); + if (text.length > country.phoneDigits) { + text = text.substring(0, country.phoneDigits); + } final buffer = StringBuffer(); - for (int i = 0; i < text.length; i++) { - if (i == 0) buffer.write('('); - buffer.write(text[i]); - if (i == 2) buffer.write(') '); - if (i == 5) buffer.write('-'); + int digitIdx = 0; + + for (int i = 0; i < country.phoneGroupSizes.length; i++) { + if (digitIdx >= text.length) break; + + buffer.write(country.phoneGroupSeparators[i]); + + final groupSize = country.phoneGroupSizes[i]; + final remainingDigits = text.length - digitIdx; + final digitsToTake = remainingDigits < groupSize ? remainingDigits : groupSize; + + buffer.write(text.substring(digitIdx, digitIdx + digitsToTake)); + digitIdx += digitsToTake; + + if (digitIdx == text.length && i < country.phoneGroupSeparators.length - 1) { + } + } + + if (digitIdx == text.length && text.length == country.phoneDigits) { + if (country.phoneGroupSeparators.length > country.phoneGroupSizes.length) { + buffer.write(country.phoneGroupSeparators.last); + } } final formattedText = buffer.toString(); diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index d2e7c66..5358b67 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -314,13 +314,13 @@ class _ChatListScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isSelected ? const Color(0xFFC1C4FF) : const Color(0xFF1E1E2A), - borderRadius: BorderRadius.circular(15), + borderRadius: BorderRadius.circular(10), ), child: Text( title, style: TextStyle( - color: isSelected ? Colors.black : Colors.white54, - fontSize: 13, + color: isSelected ? Colors.black : Color(0xFFC1C4FF), + fontSize: 14, fontWeight: FontWeight.w500, ), ), diff --git a/lib/main.dart b/lib/main.dart index f273ad7..0bd0220 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:komet/frontend/screens/auth/login_screen.dart'; -void main() { +void main() async { runApp(const MyApp()); } diff --git a/pubspec.lock b/pubspec.lock index 150913d..6a90adc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -155,6 +155,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -339,6 +347,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" sky_engine: dependency: transitive description: flutter @@ -424,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" xdg_directories: dependency: transitive description: From f843f467e9614d6c5606861fa2bb6233e84271b5 Mon Sep 17 00:00:00 2001 From: noxzion Date: Tue, 17 Mar 2026 16:36:58 +0500 Subject: [PATCH 5/9] =?UTF-8?q?=D0=AD=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B,=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8=D0=BA=20=D1=86?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/gradle.properties | 1 + .../auth/code_confirmation_screen.dart | 37 +- lib/frontend/screens/auth/login_screen.dart | 421 ++++++++---------- .../screens/auth/select_country_screen.dart | 135 ++++++ .../screens/chats/chat_list_screen.dart | 87 ++-- lib/main.dart | 53 ++- pubspec.lock | 64 +-- pubspec.yaml | 1 + 8 files changed, 436 insertions(+), 363 deletions(-) create mode 100644 lib/frontend/screens/auth/select_country_screen.dart diff --git a/android/gradle.properties b/android/gradle.properties index fbee1d8..23bd6e3 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,3 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +kotlin.incremental=false diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index cd9861f..0ea6a65 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -53,7 +53,6 @@ class _CodeConfirmationScreenState extends State { void _resendCode() { if (_timerSeconds == 0) { _startTimer(); - // TODO: Implement actual resend logic here print('Resending code to ${widget.phoneNumber}'); } } @@ -68,13 +67,14 @@ class _CodeConfirmationScreenState extends State { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: const Color(0xFF0D0D12), + backgroundColor: cs.surface, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white70), + icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant), onPressed: () => Navigator.pop(context), ), ), @@ -88,16 +88,16 @@ class _CodeConfirmationScreenState extends State { Text( widget.phoneNumber, style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 22, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 12), - const Text( + Text( 'Мы отправили SMS с кодом подтверждения на ваш номер телефона.', style: TextStyle( - color: Colors.white54, + color: cs.outline, fontSize: 15, fontWeight: FontWeight.w400, height: 1.4, @@ -129,7 +129,6 @@ class _CodeConfirmationScreenState extends State { ), ), ), - GestureDetector( onTap: () => _focusNode.requestFocus(), child: FittedBox( @@ -144,12 +143,12 @@ class _CodeConfirmationScreenState extends State { height: 54, margin: EdgeInsets.only(right: index == 4 ? 0 : 10), decoration: BoxDecoration( - color: const Color(0xFF1E1E2A), + color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), border: Border.all( color: isFocused - ? const Color(0xFFBEC2FF) - : (hasValue ? Colors.white24 : Colors.transparent), + ? cs.primary + : (hasValue ? cs.outlineVariant : Colors.transparent), width: 1.5, ), ), @@ -165,8 +164,8 @@ class _CodeConfirmationScreenState extends State { child: Text( char, key: ValueKey(char + index.toString()), - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w600, ), @@ -183,11 +182,11 @@ class _CodeConfirmationScreenState extends State { GestureDetector( onTap: _resendCode, child: Text( - _timerSeconds > 0 - ? 'Отправить повторно через $_timerSeconds сек.' + _timerSeconds > 0 + ? 'Отправить повторно через $_timerSeconds сек.' : 'Отправить код по SMS', - style: const TextStyle( - color: Color(0xFF63A9F5), + style: TextStyle( + color: cs.tertiary, fontSize: 14, fontWeight: FontWeight.w400, ), @@ -204,15 +203,15 @@ class _CodeConfirmationScreenState extends State { } }, backgroundColor: _codeController.text.length == 5 - ? const Color(0xffc1c4ff) - : const Color(0xFF1E1E2A), + ? cs.primaryContainer + : cs.surfaceContainerHighest, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), child: Icon( Icons.arrow_forward, - color: _codeController.text.length == 5 ? Colors.black : Colors.white24, + color: _codeController.text.length == 5 ? cs.onPrimaryContainer : cs.onSurfaceVariant, ), ), ], diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index d950724..fb375b4 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; import 'code_confirmation_screen.dart'; +import 'select_country_screen.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -24,228 +25,176 @@ class _LoginScreenState extends State { _selectedCountry = countriesByCode['RU'] ?? allCountries.first; } - void _showCountryPicker() { - showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFF1E1E2A), - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.5, - maxChildSize: 1.0, - snap: true, - snapSizes: const [0.7, 1.0], - expand: false, - builder: (context, scrollController) => Column( - children: [ - Padding( - padding: const EdgeInsets.all(24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Выберите страну', - style: GoogleFonts.inter( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Symbols.close, color: Colors.white70), - ), - ], - ), - ), - Expanded( - child: ListView.builder( - controller: scrollController, - itemCount: allCountries.length, - itemBuilder: (context, index) { - final country = allCountries[index]; - return ListTile( - leading: Text( - country.phoneCode, - style: const TextStyle(color: Colors.white54, fontSize: 14), - ), - title: Text( - country.ru, - style: GoogleFonts.inter( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w400, - ), - ), - onTap: () { - setState(() { - _selectedCountry = country; - _phoneController.clear(); - _isPhoneValid = false; - }); - Navigator.pop(context); - }, - ); - }, - ), - ), - ], + void _showCountryPicker() async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SelectCountryScreen( + selectedCountry: _selectedCountry, ), ), ); + if (result != null) { + setState(() { + _selectedCountry = result; + _phoneController.clear(); + _isPhoneValid = false; + }); + } } void _showTOS(BuildContext context) { + final cs = Theme.of(context).colorScheme; showModalBottomSheet( context: context, - backgroundColor: const Color(0xFF1E1E2A), + backgroundColor: cs.surfaceContainerHigh, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.5, - maxChildSize: 1.0, - snap: true, - snapSizes: const [0.7, 1.0], - expand: false, - builder: (context, scrollController) => Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Условия использования', - style: GoogleFonts.inter( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Symbols.close, color: Colors.white70), - ), - ], - ), - const SizedBox(height: 16), - Expanded( - child: ListView( - controller: scrollController, - children: const [ + builder: (context) { + final cs = Theme.of(context).colorScheme; + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 1.0, + snap: true, + snapSizes: const [0.7, 1.0], + expand: false, + builder: (context, scrollController) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' - '1. Статус и отношения\n' - '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' - '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' - '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' - '2. Условия использования\n' - '2.1. Используя Приложение «Komet», вы:\n' - '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' - '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' - '• Принимаете на себя все риски, связанные с использованием Приложения.\n' - '2.2. Строго запрещено:\n' - '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' - '• Осуществлять массовые рассылки (спам);\n' - '• Нарушать законодательство РФ и международное право;\n' - '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' - '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' - '2.4. Особенности технического взаимодействия:\n' - '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' - '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' - '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' - '3. Технические аспекты\n' - '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' - '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' - '3.3. Разработчики не несут ответственности за:\n' - '• Изменения в API оригинального сервиса;\n' - '• Блокировку аккаунтов пользователей;\n' - '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' - '4. Конфиденциальность\n' - '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' - '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' - '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' - '5. Ответственность и ограничения\n' - '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' - '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' - '6. Правовые основания\n' - '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' - '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' - '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' - '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' - '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' - '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' - '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' - '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' - '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' - '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' - '7. Заключительные положения\n' - '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' - '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' - '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' - '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' - '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' - '8. Функции безопасности и конфиденциальности\n' - '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' - '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' - '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' - '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' - '8.2. Данные функции:\n' - '• Направлены исключительно на защиту конфиденциальности пользователей;\n' - '• Не используются для обхода систем безопасности оригинального сервиса;\n' - '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' - '8.3. Разработчики не несут ответственности за:\n' - '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' - '• Изменения в работе сервиса при активации данных функций.\n' - '8.4. Функции экспорта и импорта сессии\n' - '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' - '• Обеспечения переносимости данных между устройствами пользователя\n' - '• Резервного копирования учетных данных\n' - '• Восстановления доступа при утере устройства\n' - '8.4.2. Особенности реализации:\n' - '• Экспорт сессии осуществляется без привязки к номеру телефона\n' - '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' - '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' - '8.4.3. Техническая реализация экспорта сессии:\n' - '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' - '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' - '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' - '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' - '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' - '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' - '8.4.4. Правовые основания:\n' - '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' - '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' - '• Принцип минимизации данных — сбор только необходимой для работы информации\n' - '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' - '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' - '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' - '8.4.5. Ограничения ответственности:\n' - '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' - '• Разработчики не имеют доступа к зашифрованным данным сессии\n' - '• Восстановление утерянных паролей невозможно в целях безопасности\n' - '• Ключи шифрования не хранятся в приложении и известны только пользователю', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.5, + 'Условия использования', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, ), ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Symbols.close, color: cs.onSurfaceVariant), + ), ], ), - ), - ], + const SizedBox(height: 16), + Expanded( + child: ListView( + controller: scrollController, + children: [ + Text( + 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' + '1. Статус и отношения\n' + '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' + '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' + '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' + '2. Условия использования\n' + '2.1. Используя Приложение «Komet», вы:\n' + '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' + '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' + '• Принимаете на себя все риски, связанные с использованием Приложения.\n' + '2.2. Строго запрещено:\n' + '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' + '• Осуществлять массовые рассылки (спам);\n' + '• Нарушать законодательство РФ и международное право;\n' + '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' + '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' + '2.4. Особенности технического взаимодействия:\n' + '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' + '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' + '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' + '3. Технические аспекты\n' + '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' + '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' + '3.3. Разработчики не несут ответственности за:\n' + '• Изменения в API оригинального сервиса;\n' + '• Блокировку аккаунтов пользователей;\n' + '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' + '4. Конфиденциальность\n' + '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' + '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' + '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' + '5. Ответственность и ограничения\n' + '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' + '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' + '6. Правовые основания\n' + '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' + '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' + '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' + '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' + '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' + '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' + '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' + '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' + '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' + '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' + '7. Заключительные положения\n' + '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' + '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' + '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' + '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' + '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' + '8. Функции безопасности и конфиденциальности\n' + '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' + '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' + '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' + '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' + '8.2. Данные функции:\n' + '• Направлены исключительно на защиту конфиденциальности пользователей;\n' + '• Не используются для обхода систем безопасности оригинального сервиса;\n' + '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' + '8.3. Разработчики не несут ответственности за:\n' + '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' + '• Изменения в работе сервиса при активации данных функций.\n' + '8.4. Функции экспорта и импорта сессии\n' + '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' + '• Обеспечения переносимости данных между устройствами пользователя\n' + '• Резервного копирования учетных данных\n' + '• Восстановления доступа при утере устройства\n' + '8.4.2. Особенности реализации:\n' + '• Экспорт сессии осуществляется без привязки к номеру телефона\n' + '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' + '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' + '8.4.3. Техническая реализация экспорта сессии:\n' + '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' + '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' + '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' + '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' + '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' + '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' + '8.4.4. Правовые основания:\n' + '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' + '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' + '• Принцип минимизации данных — сбор только необходимой для работы информации\n' + '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' + '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' + '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' + '8.4.5. Ограничения ответственности:\n' + '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' + '• Разработчики не имеют доступа к зашифрованным данным сессии\n' + '• Восстановление утерянных паролей невозможно в целях безопасности\n' + '• Ключи шифрования не хранятся в приложении и известны только пользователю', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + ], + ), + ), + ], + ), ), - ), - ), + ); + }, ); } @@ -258,6 +207,7 @@ class _LoginScreenState extends State { transitionDuration: const Duration(milliseconds: 250), pageBuilder: (context, anim1, anim2) => const SizedBox.shrink(), transitionBuilder: (context, anim1, anim2, child) { + final cs = Theme.of(context).colorScheme; final curve = Curves.easeOutQuart.transform(anim1.value); return Opacity( opacity: anim1.value, @@ -266,7 +216,7 @@ class _LoginScreenState extends State { child: Transform.scale( scale: 0.8 + (0.2 * curve), child: AlertDialog( - backgroundColor: const Color(0xFF1E1E2A), + backgroundColor: cs.surfaceContainerHigh, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), @@ -280,7 +230,7 @@ class _LoginScreenState extends State { Text( 'Это правильный номер?', style: GoogleFonts.inter( - color: Colors.white70, + color: cs.onSurfaceVariant, fontSize: 14, fontWeight: FontWeight.w500, ), @@ -289,7 +239,7 @@ class _LoginScreenState extends State { Text( '${_selectedCountry.phoneCode} $formattedPhone', style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w500, ), @@ -305,7 +255,7 @@ class _LoginScreenState extends State { child: Text( 'Изменить', style: GoogleFonts.inter( - color: const Color(0xFFAFAFFF), + color: cs.primary, fontSize: 15, fontWeight: FontWeight.w500, ), @@ -326,7 +276,7 @@ class _LoginScreenState extends State { child: Text( 'Готово', style: GoogleFonts.inter( - color: const Color(0xFFAFAFFF), + color: cs.primary, fontSize: 15, fontWeight: FontWeight.w600, ), @@ -349,8 +299,9 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: const Color(0xFF0D0D12), + backgroundColor: cs.surface, body: GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragEnd: (details) { @@ -376,17 +327,17 @@ class _LoginScreenState extends State { children: [ IconButton( onPressed: () {}, - icon: const Icon( + icon: Icon( Symbols.admin_panel_settings, - color: Colors.white70, + color: cs.onSurfaceVariant, weight: 400, ), ), IconButton( onPressed: () {}, - icon: const Icon( + icon: Icon( Symbols.language, - color: Colors.white70, + color: cs.onSurfaceVariant, weight: 400, ), ), @@ -399,23 +350,23 @@ class _LoginScreenState extends State { Image.asset( 'assets/komet.png', height: 80, - color: Colors.white, + color: cs.onSurface, ), const SizedBox(height: 16), Text( 'Войдите в Komet', style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), - const Text( + Text( 'Проверьте код страны и введите свой\nномер телефона.', textAlign: TextAlign.center, style: TextStyle( - color: Colors.white70, + color: cs.onSurfaceVariant, fontSize: 16, fontWeight: FontWeight.w400, height: 1.4, @@ -435,13 +386,13 @@ class _LoginScreenState extends State { Text( _selectedCountry.ru, style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400, ), ), const Spacer(), - const Icon(Icons.keyboard_arrow_down, color: Colors.white70), + Icon(Icons.keyboard_arrow_down, color: cs.onSurfaceVariant), ], ), ), @@ -454,7 +405,7 @@ class _LoginScreenState extends State { Text( _selectedCountry.phoneCode, style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400, ), @@ -463,7 +414,7 @@ class _LoginScreenState extends State { Container( width: 1, height: 24, - color: Colors.white24, + color: cs.outlineVariant, ), const SizedBox(width: 12), Expanded( @@ -474,10 +425,10 @@ class _LoginScreenState extends State { FilteringTextInputFormatter.digitsOnly, _PhoneInputFormatter(_selectedCountry), ], - style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400), - decoration: const InputDecoration( + style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400), + decoration: InputDecoration( hintText: '(000) 000-00-00', - hintStyle: TextStyle(color: Colors.white38, fontSize: 15, fontWeight: FontWeight.w400), + hintStyle: TextStyle(color: cs.outline, fontSize: 15, fontWeight: FontWeight.w400), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, @@ -499,7 +450,7 @@ class _LoginScreenState extends State { child: Text( 'Другие способы входа', style: GoogleFonts.inter( - color: const Color(0xFFAFAFFF), + color: cs.primary, fontSize: 14, fontWeight: FontWeight.w400, height: 1.4, @@ -516,7 +467,7 @@ class _LoginScreenState extends State { child: RichText( text: TextSpan( style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, height: 1.4, @@ -525,7 +476,7 @@ class _LoginScreenState extends State { TextSpan( text: 'Продолжая, вы соглашаетесь с \n', style: GoogleFonts.inter( - color: Colors.white, + color: cs.onSurface, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -534,7 +485,7 @@ class _LoginScreenState extends State { TextSpan( text: 'пользовательскими соглашениями', style: GoogleFonts.inter( - color: const Color(0xFFAFAFFF), + color: cs.primary, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -551,11 +502,11 @@ class _LoginScreenState extends State { padding: const EdgeInsets.only(bottom: 16.0), child: FloatingActionButton( onPressed: _isPhoneValid ? _validateAndSubmit : null, - backgroundColor: _isPhoneValid ? const Color(0xffc1c4ff) : Colors.white10, + backgroundColor: _isPhoneValid ? cs.primaryContainer : cs.surfaceContainerHighest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), - child: Icon(Icons.arrow_forward, color: _isPhoneValid ? Colors.black : Colors.white24), + child: Icon(Icons.arrow_forward, color: _isPhoneValid ? cs.onPrimaryContainer : cs.onSurfaceVariant), ), ), ], @@ -574,11 +525,11 @@ class _LoginScreenState extends State { ); } - Widget _buildInputField({ required String label, required Widget content, }) { + final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -591,7 +542,7 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( border: Border.all( - color: const Color(0xFFBEC2FF), + color: cs.primary, width: 1.5, ), borderRadius: BorderRadius.circular(50), @@ -602,12 +553,12 @@ class _LoginScreenState extends State { top: -10, left: 20, child: Container( - color: const Color(0xFF0D0D12), + color: cs.surface, padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( label, style: GoogleFonts.inter( - color: const Color(0xFFBEC2FF), + color: cs.primary, fontSize: 14, fontWeight: FontWeight.w400, ), diff --git a/lib/frontend/screens/auth/select_country_screen.dart b/lib/frontend/screens/auth/select_country_screen.dart new file mode 100644 index 0000000..a72335e --- /dev/null +++ b/lib/frontend/screens/auth/select_country_screen.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:komet/core/config/countries.dart'; + +class SelectCountryScreen extends StatefulWidget { + final CountryName selectedCountry; + + const SelectCountryScreen({super.key, required this.selectedCountry}); + + @override + State createState() => _SelectCountryScreenState(); +} + +class _SelectCountryScreenState extends State { + bool _isSearching = false; + final TextEditingController _searchController = TextEditingController(); + List _filteredCountries = allCountries; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _filterCountries(String query) { + setState(() { + if (query.isEmpty) { + _filteredCountries = allCountries; + } else { + final q = query.toLowerCase(); + _filteredCountries = allCountries.where((c) { + return c.ru.toLowerCase().contains(q) || + c.en.toLowerCase().contains(q) || + c.phoneCode.contains(q); + }).toList(); + } + }); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + backgroundColor: cs.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Symbols.arrow_back, color: cs.onSurface), + ), + title: _isSearching + ? TextField( + controller: _searchController, + autofocus: true, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + hintText: 'Поиск страны...', + hintStyle: GoogleFonts.inter( + color: cs.onSurfaceVariant, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + border: InputBorder.none, + ), + onChanged: _filterCountries, + ) + : Text( + 'Выберите страну', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + centerTitle: true, + actions: [ + IconButton( + onPressed: () { + setState(() { + _isSearching = !_isSearching; + if (!_isSearching) { + _searchController.clear(); + _filteredCountries = allCountries; + } + }); + }, + icon: Icon( + _isSearching ? Symbols.close : Symbols.search, + color: cs.onSurface, + ), + ), + ], + ), + body: ListView.builder( + itemCount: _filteredCountries.length, + itemBuilder: (context, index) { + final country = _filteredCountries[index]; + final isSelected = country.code == widget.selectedCountry.code; + + return ListTile( + leading: Text( + country.phoneCode, + style: GoogleFonts.inter( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + title: Text( + country.ru, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + trailing: isSelected + ? Icon(Symbols.check, color: cs.primary) + : null, + onTap: () { + Navigator.pop(context, country); + }, + ); + }, + ), + ); + } +} diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 5358b67..4260cd8 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -15,8 +15,9 @@ class _ChatListScreenState extends State { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: const Color(0xFF0D0D12), + backgroundColor: cs.surface, body: SafeArea( bottom: false, child: Stack( @@ -29,10 +30,10 @@ class _ChatListScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Подключение...', style: TextStyle( - color: Colors.white, + color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w600, fontFamily: 'Outfit', @@ -40,9 +41,9 @@ class _ChatListScreenState extends State { ), IconButton( onPressed: () {}, - icon: const Icon( + icon: Icon( Symbols.more_vert, - color: Colors.white54, + color: cs.outline, weight: 400, ), ), @@ -63,27 +64,26 @@ class _ChatListScreenState extends State { ), ), ), - SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), sliver: SliverToBoxAdapter( child: Container( height: 44, decoration: BoxDecoration( - color: const Color(0xFF1E1E2A), + color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(50), ), padding: const EdgeInsets.symmetric(horizontal: 16), - child: const Row( + child: Row( children: [ - Icon(Symbols.search, color: Colors.white38, size: 20, weight: 400), - SizedBox(width: 10), + Icon(Symbols.search, color: cs.outline, size: 20, weight: 400), + const SizedBox(width: 10), Expanded( child: TextField( - style: TextStyle(color: Colors.white, fontSize: 15), + style: TextStyle(color: cs.onSurface, fontSize: 15), decoration: InputDecoration( hintText: 'Поиск', - hintStyle: TextStyle(color: Colors.white38, fontSize: 15), + hintStyle: TextStyle(color: cs.outline, fontSize: 15), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, @@ -95,7 +95,6 @@ class _ChatListScreenState extends State { ), ), ), - SliverToBoxAdapter( child: SizedBox( height: 48, @@ -187,11 +186,11 @@ class _ChatListScreenState extends State { height: 68, padding: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( - color: const Color(0xFF1E1E28), + color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(34), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10), ), @@ -200,17 +199,17 @@ class _ChatListScreenState extends State { child: LayoutBuilder( builder: (context, constraints) { double totalWidth = constraints.maxWidth; - + double totalWeight = 5.2; double unitWidth = totalWidth / totalWeight; double activeWidth = unitWidth * 2.2; double inactiveWidth = unitWidth * 1.0; - + double leftOffset = 0; for (int i = 0; i < _currentNavIndex; i++) { leftOffset += inactiveWidth; } - + return Stack( children: [ AnimatedPositioned( @@ -222,7 +221,7 @@ class _ChatListScreenState extends State { width: activeWidth - 8, child: Container( decoration: BoxDecoration( - color: const Color(0xFFBEC2FF), + color: cs.primary, borderRadius: BorderRadius.circular(26), ), ), @@ -237,7 +236,7 @@ class _ChatListScreenState extends State { case 2: icon = Symbols.person_pin; label = 'Контакты'; break; default: icon = Symbols.settings; label = 'Настройки'; } - + bool isSelected = _currentNavIndex == index; return AnimatedContainer( duration: const Duration(milliseconds: 350), @@ -261,11 +260,11 @@ class _ChatListScreenState extends State { bottom: 110, child: FloatingActionButton( onPressed: () {}, - backgroundColor: const Color(0xFFC1C4FF), + backgroundColor: cs.primaryContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), - child: const Icon(Symbols.add, color: Colors.black, size: 28, weight: 400), + child: Icon(Symbols.add, color: cs.onPrimaryContainer, size: 28, weight: 400), ), ), ], @@ -275,6 +274,7 @@ class _ChatListScreenState extends State { } Widget _buildStoryItem(String name, String imageUrl, bool hasUpdate) { + final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(right: 16), child: Column( @@ -284,8 +284,8 @@ class _ChatListScreenState extends State { decoration: BoxDecoration( shape: BoxShape.circle, border: hasUpdate - ? Border.all(color: const Color(0xFFC1C4FF), width: 2) - : Border.all(color: Colors.white10), + ? Border.all(color: cs.primary, width: 2) + : Border.all(color: cs.outlineVariant), ), child: CircleAvatar( radius: 26, @@ -295,8 +295,8 @@ class _ChatListScreenState extends State { const SizedBox(height: 6), Text( name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.w500, ), @@ -307,19 +307,20 @@ class _ChatListScreenState extends State { } Widget _buildFolderChip(String title) { + final cs = Theme.of(context).colorScheme; bool isSelected = _selectedCategory == title; return GestureDetector( onTap: () => setState(() => _selectedCategory = title), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( - color: isSelected ? const Color(0xFFC1C4FF) : const Color(0xFF1E1E2A), + color: isSelected ? cs.primaryContainer : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(10), ), child: Text( title, style: TextStyle( - color: isSelected ? Colors.black : Color(0xFFC1C4FF), + color: isSelected ? cs.onPrimaryContainer : cs.primary, fontSize: 14, fontWeight: FontWeight.w500, ), @@ -339,6 +340,7 @@ class _ChatListScreenState extends State { int unreadCount = 0, bool isMuted = false, }) { + final cs = Theme.of(context).colorScheme; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), leading: Stack( @@ -355,9 +357,9 @@ class _ChatListScreenState extends State { width: 12, height: 12, decoration: BoxDecoration( - color: const Color(0xFFC1C4FF), + color: cs.primary, shape: BoxShape.circle, - border: Border.all(color: const Color(0xFF0D0D12), width: 2), + border: Border.all(color: cs.surface, width: 2), ), ), ), @@ -368,20 +370,20 @@ class _ChatListScreenState extends State { Expanded( child: Text( name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600, ), ), ), if (isMuted) - const Icon(Symbols.notifications_off, color: Colors.white24, size: 14, weight: 400), + Icon(Symbols.notifications_off, color: cs.outlineVariant, size: 14, weight: 400), const SizedBox(width: 8), Text( time, - style: const TextStyle( - color: Colors.white38, + style: TextStyle( + color: cs.outline, fontSize: 12, ), ), @@ -395,7 +397,7 @@ class _ChatListScreenState extends State { child: Text( message, style: TextStyle( - color: isTyping ? const Color(0xFFC1C4FF) : Colors.white54, + color: isTyping ? cs.primary : cs.outline, fontSize: 14, fontWeight: isTyping ? FontWeight.w500 : FontWeight.w400, ), @@ -407,20 +409,20 @@ class _ChatListScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isMuted ? Colors.white10 : const Color(0xFF1E1E2A), + color: isMuted ? cs.surfaceContainerHighest : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(10), ), child: Text( unreadCount.toString(), style: TextStyle( - color: isMuted ? Colors.white38 : Colors.white, + color: isMuted ? cs.outline : cs.onSurface, fontSize: 11, fontWeight: FontWeight.w600, ), ), ) else if (isRead) - const Icon(Symbols.done_all, color: Color(0xFFC1C4FF), size: 16, weight: 400), + Icon(Symbols.done_all, color: cs.primary, size: 16, weight: 400), ], ), ), @@ -428,6 +430,7 @@ class _ChatListScreenState extends State { } Widget _buildNavItem(int index, IconData icon, String label) { + final cs = Theme.of(context).colorScheme; bool isSelected = _currentNavIndex == index; return GestureDetector( onTap: () => setState(() => _currentNavIndex = index), @@ -441,7 +444,7 @@ class _ChatListScreenState extends State { children: [ Icon( icon, - color: isSelected ? Colors.black : Colors.white, + color: isSelected ? cs.onPrimary : cs.onSurface, size: 20, weight: 400, fill: isSelected ? 1.0 : 0.0, @@ -459,8 +462,8 @@ class _ChatListScreenState extends State { const SizedBox(width: 4), Text( label, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: cs.onPrimary, fontSize: 13, fontWeight: FontWeight.w600, ), diff --git a/lib/main.dart b/lib/main.dart index 0bd0220..3833514 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:komet/frontend/screens/auth/login_screen.dart'; @@ -9,19 +10,49 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({super.key}); + static const _fallbackSeed = Color(0xFFC1C4FF); + + static ColorScheme _adjustScheme(ColorScheme base) { + return base.copyWith( + surface: Color.alphaBlend( + base.primary.withValues(alpha: 0.05), + const Color(0xFF0D0D14), + ), + surfaceContainerHigh: Color.alphaBlend( + base.primary.withValues(alpha: 0.08), + const Color(0xFF1A1A26), + ), + surfaceContainerHighest: Color.alphaBlend( + base.primary.withValues(alpha: 0.12), + const Color(0xFF262636), + ), + ); + } + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Komet', - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - textTheme: GoogleFonts.interTextTheme( - ThemeData.dark().textTheme, - ), - ), - home: const LoginScreen(), + return DynamicColorBuilder( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + final baseScheme = darkDynamic ?? ColorScheme.fromSeed( + seedColor: _fallbackSeed, + brightness: Brightness.dark, + ); + + final darkScheme = _adjustScheme(baseScheme); + + return MaterialApp( + title: 'Komet', + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + colorScheme: darkScheme, + textTheme: GoogleFonts.interTextTheme( + ThemeData.dark().textTheme, + ), + ), + home: const LoginScreen(), + ); + }, ); } } diff --git a/pubspec.lock b/pubspec.lock index 6a90adc..e7c44c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + url: "https://pub.dev" + source: hosted + version: "1.8.1" fake_async: dependency: transitive description: @@ -155,14 +163,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.dev" - source: hosted - version: "4.11.0" leak_tracker: dependency: transitive description: @@ -347,46 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - screen_retriever: - dependency: transitive - description: - name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_linux: - dependency: transitive - description: - name: screen_retriever_linux - sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_macos: - dependency: transitive - description: - name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_platform_interface: - dependency: transitive - description: - name: screen_retriever_platform_interface - sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_windows: - dependency: transitive - description: - name: screen_retriever_windows - sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.dev" - source: hosted - version: "0.2.0" sky_engine: dependency: transitive description: flutter @@ -472,14 +432,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - window_manager: - dependency: "direct main" - description: - name: window_manager - sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" - url: "https://pub.dev" - source: hosted - version: "0.5.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b832e79..60d2dc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: logger: ^2.6.2 google_fonts: ^6.2.1 material_symbols_icons: ^4.2906.0 + dynamic_color: ^1.8.1 dev_dependencies: flutter_test: From 0306820746d5c04b01b4eef42f18bf70a3de27f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Mon, 23 Mar 2026 18:44:52 +0700 Subject: [PATCH 6/9] =?UTF-8?q?=D0=92=D1=80=D0=BE=D0=B4=D0=B5=20=D1=87?= =?UTF-8?q?=D0=B5=D1=82=20=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B2=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B5=20=D0=B2=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/logger.dart | 2 +- lib/frontend/screens/auth/login_screen.dart | 916 +++++++++++++------- pubspec.lock | 77 +- pubspec.yaml | 1 + 4 files changed, 673 insertions(+), 323 deletions(-) diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 5fbcd7b..65ddcb5 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -1,4 +1,4 @@ -import 'package:logger/logger.dart'; +import 'package:logger/logger.dart'; //нет это не опасный вредоносный вирус логер который украдёт ваш аккаунт Browl Starz final logger = Logger( // устанавливает минимальный уровень логов. diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index fb375b4..44a8de4 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -4,9 +4,89 @@ import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'code_confirmation_screen.dart'; import 'select_country_screen.dart'; +void _showCustomNotification(BuildContext context, String message) { + final overlay = Overlay.of(context); + final entry = OverlayEntry( + builder: (context) => _CustomNotification(message: message), + ); + overlay.insert(entry); + Future.delayed(const Duration(milliseconds: 1900), () { + entry.remove(); + }); +} + +class _CustomNotification extends StatefulWidget { + final String message; + const _CustomNotification({required this.message}); + + @override + State<_CustomNotification> createState() => _CustomNotificationState(); +} + +class _CustomNotificationState extends State<_CustomNotification> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + ); + _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller); + _controller.forward(); + Future.delayed(const Duration(milliseconds: 1600), () { + if (mounted) _controller.reverse(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Positioned( + bottom: 60, + left: 0, + right: 0, + child: Material( + color: Colors.transparent, + child: Center( + child: FadeTransition( + opacity: _opacity, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + widget.message, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } +} + class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -18,20 +98,40 @@ class _LoginScreenState extends State { final TextEditingController _phoneController = TextEditingController(); late CountryName _selectedCountry; bool _isPhoneValid = false; + bool _isTOSRead = false; @override void initState() { super.initState(); _selectedCountry = countriesByCode['RU'] ?? allCountries.first; + _checkTOS(); + } + + Future _checkTOS() async { + final prefs = await SharedPreferences.getInstance(); + if (mounted) { + setState(() { + _isTOSRead = prefs.getBool('IsReadeTOS') ?? false; + }); + } + } + + Future _markTOSRead() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('IsReadeTOS', true); + if (mounted) { + setState(() { + _isTOSRead = true; + }); + } } void _showCountryPicker() async { final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => SelectCountryScreen( - selectedCountry: _selectedCountry, - ), + builder: (context) => + SelectCountryScreen(selectedCountry: _selectedCountry), ), ); if (result != null) { @@ -53,146 +153,232 @@ class _LoginScreenState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) { - final cs = Theme.of(context).colorScheme; - return DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.5, - maxChildSize: 1.0, - snap: true, - snapSizes: const [0.7, 1.0], - expand: false, - builder: (context, scrollController) => Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + double progress = _isTOSRead ? 1.0 : 0.0; + return StatefulBuilder( + builder: (context, setModalState) { + final cs = Theme.of(context).colorScheme; + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 1.0, + snap: true, + snapSizes: const [0.7, 1.0], + expand: false, + builder: (context, scrollController) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Условия использования', - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.w500, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Условия использования', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Symbols.close, color: cs.onSurfaceVariant), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: NotificationListener( + onNotification: + (ScrollUpdateNotification notification) { + if (_isTOSRead) return false; + final metrics = notification.metrics; + if (metrics.maxScrollExtent > 0) { + double newProgress = + metrics.pixels / metrics.maxScrollExtent; + newProgress = newProgress.clamp(0.0, 1.0); + if (newProgress >= 0.99 && !_isTOSRead) { + _markTOSRead(); + setModalState(() { + progress = 1.0; + }); + } else { + setModalState(() { + progress = newProgress; + }); + } + } + return false; + }, + child: ListView( + controller: scrollController, + children: [ + //У мя вопрос а тут точно номера статей верные??? + Text( + 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' + '1. Статус и отношения\n' + '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' + '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' + '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' + '2. Условия использования\n' + '2.1. Используя Приложение «Komet», вы:\n' + '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' + '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' + '• Принимаете на себя все риски, связанные с использованием Приложения.\n' + '2.2. Строго запрещено:\n' + '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' + '• Осуществлять массовые рассылки (спам);\n' + '• Нарушать законодательство РФ и международное право;\n' + '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' + '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' + '2.4. Особенности технического взаимодействия:\n' + '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' + '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' + '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' + '3. Технические аспекты\n' + '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' + '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' + '3.3. Разработчики не несут ответственности за:\n' + '• Изменения в API оригинального сервиса;\n' + '• Блокировку аккаунтов пользователей;\n' + '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' + '4. Конфиденциальность\n' + '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' + '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' + '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' + '5. Ответственность и ограничения\n' + '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' + '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' + '6. Правовые основания\n' + '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' + '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' + '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' + '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' + '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' + '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' + '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' + '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' + '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' + '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' + '7. Заключительные положения\n' + '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' + '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' + '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' + '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' + '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' + '8. Функции безопасности и конфиденциальности\n' + '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' + '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' + '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' + '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' + '8.2. Данные функции:\n' + '• Направлены исключительно на защиту конфиденциальности пользователей;\n' + '• Не используются для обхода систем безопасности оригинального сервиса;\n' + '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' + '8.3. Разработчики не несут ответственности за:\n' + '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' + '• Изменения в работе сервиса при активации данных функций.\n' + '8.4. Функции экспорта и импорта сессии\n' + '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' + '• Обеспечения переносимости данных между устройствами пользователя\n' + '• Резервного копирования учетных данных\n' + '• Восстановления доступа при утере устройства\n' + '8.4.2. Особенности реализации:\n' + '• Экспорт сессии осуществляется без привязки к номеру телефона\n' + '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' + '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' + '8.4.3. Техническая реализация экспорта сессии:\n' + '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' + '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' + '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' + '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' + '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' + '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' + '8.4.4. Правовые основания:\n' + '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' + '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' + '• Принцип минимизации данных — сбор только необходимой для работы информации\n' + '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' + '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' + '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' + '8.4.5. Ограничения ответственности:\n' + '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' + '• Разработчики не имеют доступа к зашифрованным данным сессии\n' + '• Восстановление утерянных паролей невозможно в целях безопасности\n' + '• Ключи шифрования не хранятся в приложении и известны только пользователю', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + const SizedBox(height: 32), + ], + ), ), ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon(Symbols.close, color: cs.onSurfaceVariant), + const SizedBox(height: 16), + Stack( + alignment: Alignment.centerRight, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 48, + alignment: Alignment.centerLeft, + padding: EdgeInsets.only( + right: progress == 1.0 ? 56.0 : 0.0, + ), + child: Container( + height: 4, + width: double.infinity, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(2), + ), + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: progress < 1.0 ? progress : 1.0, + child: Container( + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ), + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: progress == 1.0 ? 1.0 : 0.0, + curve: Curves.easeIn, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + transform: Matrix4.translationValues( + progress == 1.0 ? 0 : 20, + 0, + 0, + ), + width: 40, + height: 40, + decoration: BoxDecoration( + color: cs.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Symbols.check, + color: cs.onPrimaryContainer, + size: 24, + ), + ), + ), + ], ), ], ), - const SizedBox(height: 16), - Expanded( - child: ListView( - controller: scrollController, - children: [ - Text( - 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' - '1. Статус и отношения\n' - '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' - '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' - '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' - '2. Условия использования\n' - '2.1. Используя Приложение «Komet», вы:\n' - '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' - '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' - '• Принимаете на себя все риски, связанные с использованием Приложения.\n' - '2.2. Строго запрещено:\n' - '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' - '• Осуществлять массовые рассылки (спам);\n' - '• Нарушать законодательство РФ и международное право;\n' - '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' - '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' - '2.4. Особенности технического взаимодействия:\n' - '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' - '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' - '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' - '3. Технические аспекты\n' - '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' - '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' - '3.3. Разработчики не несут ответственности за:\n' - '• Изменения в API оригинального сервиса;\n' - '• Блокировку аккаунтов пользователей;\n' - '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' - '4. Конфиденциальность\n' - '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' - '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' - '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' - '5. Ответственность и ограничения\n' - '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' - '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' - '6. Правовые основания\n' - '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' - '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' - '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' - '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' - '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' - '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' - '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' - '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' - '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' - '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' - '7. Заключительные положения\n' - '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' - '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' - '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' - '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' - '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' - '8. Функции безопасности и конфиденциальности\n' - '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' - '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' - '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' - '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' - '8.2. Данные функции:\n' - '• Направлены исключительно на защиту конфиденциальности пользователей;\n' - '• Не используются для обхода систем безопасности оригинального сервиса;\n' - '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' - '8.3. Разработчики не несут ответственности за:\n' - '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' - '• Изменения в работе сервиса при активации данных функций.\n' - '8.4. Функции экспорта и импорта сессии\n' - '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' - '• Обеспечения переносимости данных между устройствами пользователя\n' - '• Резервного копирования учетных данных\n' - '• Восстановления доступа при утере устройства\n' - '8.4.2. Особенности реализации:\n' - '• Экспорт сессии осуществляется без привязки к номеру телефона\n' - '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' - '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' - '8.4.3. Техническая реализация экспорта сессии:\n' - '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' - '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' - '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' - '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' - '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' - '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' - '8.4.4. Правовые основания:\n' - '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' - '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' - '• Принцип минимизации данных — сбор только необходимой для работы информации\n' - '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' - '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' - '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' - '8.4.5. Ограничения ответственности:\n' - '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' - '• Разработчики не имеют доступа к зашифрованным данным сессии\n' - '• Восстановление утерянных паролей невозможно в целях безопасности\n' - '• Ключи шифрования не хранятся в приложении и известны только пользователю', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.5, - ), - ), - ], - ), - ), - ], - ), - ), + ), + ); + }, ); }, ); @@ -268,7 +454,8 @@ class _LoginScreenState extends State { context, MaterialPageRoute( builder: (context) => CodeConfirmationScreen( - phoneNumber: '${_selectedCountry.phoneCode} $formattedPhone', + phoneNumber: + '${_selectedCountry.phoneCode} $formattedPhone', ), ), ); @@ -294,9 +481,81 @@ class _LoginScreenState extends State { } void _validateAndSubmit() { + if (!_isTOSRead) { + _showCustomNotification(context, 'Соглащение прочитай щегол'); + return; + } _showPhoneConfirmationDialog(_phoneController.text); } + void _showOtherLoginMethods(BuildContext context) { + final cs = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: cs.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Symbols.qr_code_2, color: cs.onSurface), + title: Text( + 'По QR code', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Symbols.key, color: cs.onSurface), + title: Text( + 'По токену', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Symbols.description, color: cs.onSurface), + title: Text( + 'По файлу сессии', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; @@ -305,7 +564,8 @@ class _LoginScreenState extends State { body: GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragEnd: (details) { - if (details.primaryVelocity != null && details.primaryVelocity! < -500) { + if (details.primaryVelocity != null && + details.primaryVelocity! < -500) { _showTOS(context); } }, @@ -314,199 +574,228 @@ class _LoginScreenState extends State { builder: (context, constraints) { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 44), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () {}, - icon: Icon( - Symbols.admin_panel_settings, - color: cs.onSurfaceVariant, - weight: 400, - ), - ), - IconButton( - onPressed: () {}, - icon: Icon( - Symbols.language, - color: cs.onSurfaceVariant, - weight: 400, - ), - ), - ], - ), - const SizedBox(height: 74), - Center( - child: Column( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 44), + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Image.asset( - 'assets/komet.png', - height: 80, - color: cs.onSurface, - ), - const SizedBox(height: 16), - Text( - 'Войдите в Komet', - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 32, - fontWeight: FontWeight.w500, + IconButton( + onPressed: () {}, + icon: Icon( + Symbols.admin_panel_settings, + color: cs.onSurfaceVariant, + weight: 400, ), ), - const SizedBox(height: 8), - Text( - 'Проверьте код страны и введите свой\nномер телефона.', - textAlign: TextAlign.center, - style: TextStyle( + IconButton( + onPressed: () {}, + icon: Icon( + Symbols.language, color: cs.onSurfaceVariant, - fontSize: 16, - fontWeight: FontWeight.w400, - height: 1.4, + weight: 400, ), ), ], ), - ), - const SizedBox(height: 48), - InkWell( - onTap: _showCountryPicker, - borderRadius: BorderRadius.circular(50), - child: _buildInputField( - label: 'Страна', + const SizedBox(height: 74), + Center( + child: Column( + children: [ + Image.asset( + 'assets/komet.png', + height: 80, + color: cs.onSurface, + ), + const SizedBox(height: 16), + Text( + 'Войдите в Komet', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 32, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Проверьте код страны и введите свой\nномер телефона.', + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 16, + fontWeight: FontWeight.w400, + height: 1.4, + ), + ), + ], + ), + ), + const SizedBox(height: 48), + InkWell( + onTap: _showCountryPicker, + borderRadius: BorderRadius.circular(50), + child: _buildInputField( + label: 'Страна', + content: Row( + children: [ + Text( + _selectedCountry.ru, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + ), + const Spacer(), + Icon( + Icons.keyboard_arrow_down, + color: cs.onSurfaceVariant, + ), + ], + ), + ), + ), + const SizedBox(height: 40), + _buildInputField( + label: 'Номер телефона', content: Row( children: [ Text( - _selectedCountry.ru, + _selectedCountry.phoneCode, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400, ), ), - const Spacer(), - Icon(Icons.keyboard_arrow_down, color: cs.onSurfaceVariant), + const SizedBox(width: 12), + Container( + width: 1, + height: 24, + color: cs.outlineVariant, + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _PhoneInputFormatter(_selectedCountry), + ], + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + hintText: '(000) 000-00-00', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (value) { + final digits = value.replaceAll( + RegExp(r'\D'), + '', + ); + setState(() { + _isPhoneValid = + digits.length == + _selectedCountry.phoneDigits; + }); + }, + ), + ), ], ), ), - ), - const SizedBox(height: 40), - _buildInputField( - label: 'Номер телефона', - content: Row( - children: [ - Text( - _selectedCountry.phoneCode, - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 15, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(width: 12), - Container( - width: 1, - height: 24, - color: cs.outlineVariant, - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _phoneController, - keyboardType: TextInputType.phone, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _PhoneInputFormatter(_selectedCountry), - ], - style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w400), - decoration: InputDecoration( - hintText: '(000) 000-00-00', - hintStyle: TextStyle(color: cs.outline, fontSize: 15, fontWeight: FontWeight.w400), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - onChanged: (value) { - final digits = value.replaceAll(RegExp(r'\D'), ''); - setState(() { - _isPhoneValid = digits.length == _selectedCountry.phoneDigits; - }); - }, - ), - ), - ], + const SizedBox(height: 24), + TextButton( + onPressed: () => _showOtherLoginMethods(context), + child: Text( + 'Другие способы входа', + style: GoogleFonts.inter( + color: cs.primary, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.4, ), ), - const SizedBox(height: 24), - TextButton( - onPressed: () {}, - child: Text( - 'Другие способы входа', - style: GoogleFonts.inter( - color: cs.primary, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.4, - ), ), - ), - const Spacer(), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(bottom: 24.0), - child: RichText( - text: TextSpan( - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 14, - fontWeight: FontWeight.w500, - height: 1.4, + const Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: RichText( + text: TextSpan( + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.4, + ), + children: [ + TextSpan( + text: + 'Продолжая, вы соглашаетесь с \n', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: + 'пользовательскими соглашениями', + style: GoogleFonts.inter( + color: cs.primary, + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showTOS(context), + ), + ], ), - children: [ - TextSpan( - text: 'Продолжая, вы соглашаетесь с \n', - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - TextSpan( - text: 'пользовательскими соглашениями', - style: GoogleFonts.inter( - color: cs.primary, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => _showTOS(context), - ), - ], ), ), ), - ), Padding( padding: const EdgeInsets.only(bottom: 16.0), child: FloatingActionButton( - onPressed: _isPhoneValid ? _validateAndSubmit : null, - backgroundColor: _isPhoneValid ? cs.primaryContainer : cs.surfaceContainerHighest, + onPressed: _isPhoneValid + ? _validateAndSubmit + : null, + backgroundColor: _isPhoneValid + ? cs.primaryContainer + : cs.surfaceContainerHighest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), - child: Icon(Icons.arrow_forward, color: _isPhoneValid ? cs.onPrimaryContainer : cs.onSurfaceVariant), + child: Icon( + Icons.arrow_forward, + color: _isPhoneValid + ? cs.onPrimaryContainer + : cs.onSurfaceVariant, + ), ), ), ], @@ -525,10 +814,7 @@ class _LoginScreenState extends State { ); } - Widget _buildInputField({ - required String label, - required Widget content, - }) { + Widget _buildInputField({required String label, required Widget content}) { final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -541,10 +827,7 @@ class _LoginScreenState extends State { height: 54, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - border: Border.all( - color: cs.primary, - width: 1.5, - ), + border: Border.all(color: cs.primary, width: 1.5), borderRadius: BorderRadius.circular(50), ), child: content, @@ -578,7 +861,9 @@ class _PhoneInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( - TextEditingValue oldValue, TextEditingValue newValue) { + TextEditingValue oldValue, + TextEditingValue newValue, + ) { var text = newValue.text.replaceAll(RegExp(r'\D'), ''); if (newValue.text.length < oldValue.text.length) { @@ -602,19 +887,22 @@ class _PhoneInputFormatter extends TextInputFormatter { final groupSize = country.phoneGroupSizes[i]; final remainingDigits = text.length - digitIdx; - final digitsToTake = remainingDigits < groupSize ? remainingDigits : groupSize; + final digitsToTake = remainingDigits < groupSize + ? remainingDigits + : groupSize; buffer.write(text.substring(digitIdx, digitIdx + digitsToTake)); digitIdx += digitsToTake; - if (digitIdx == text.length && i < country.phoneGroupSeparators.length - 1) { - } + if (digitIdx == text.length && + i < country.phoneGroupSeparators.length - 1) {} } if (digitIdx == text.length && text.length == country.phoneDigits) { - if (country.phoneGroupSeparators.length > country.phoneGroupSizes.length) { - buffer.write(country.phoneGroupSeparators.last); - } + if (country.phoneGroupSeparators.length > + country.phoneGroupSizes.length) { + buffer.write(country.phoneGroupSeparators.last); + } } final formattedText = buffer.toString(); diff --git a/pubspec.lock b/pubspec.lock index e7c44c3..bee7d68 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -123,6 +123,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -215,18 +220,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" material_symbols_icons: dependency: "direct main" description: @@ -347,6 +352,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + url: "https://pub.dev" + source: hosted + version: "2.4.21" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + 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 @@ -396,10 +457,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 60d2dc8..3498a9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: google_fonts: ^6.2.1 material_symbols_icons: ^4.2906.0 dynamic_color: ^1.8.1 + shared_preferences: ^2.5.4 dev_dependencies: flutter_test: From 71e776b09962abc727abe0ad494c5390944dc73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Thu, 26 Mar 2026 12:06:42 +0700 Subject: [PATCH 7/9] =?UTF-8?q?=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BB=D1=83=D1=88=D0=B5=D0=BA=20=D0=BF=D0=BE=D1=82=D1=8B?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/config/spoof_data.dart | 31 + lib/frontend/screens/auth/login_screen.dart | 145 ++-- .../screens/auth/spoff_redacted_screen.dart | 217 ++++++ lib/frontend/screens/calls/calls_tab.dart | 21 + .../screens/chats/chat_list_screen.dart | 706 ++++++++++++------ .../screens/contacts/contacts_tab.dart | 21 + .../screens/profile/settings_tab.dart | 21 + lib/frontend/widgets/custom_notification.dart | 81 ++ 8 files changed, 927 insertions(+), 316 deletions(-) create mode 100644 lib/core/config/spoof_data.dart create mode 100644 lib/frontend/screens/auth/spoff_redacted_screen.dart create mode 100644 lib/frontend/screens/calls/calls_tab.dart create mode 100644 lib/frontend/screens/contacts/contacts_tab.dart create mode 100644 lib/frontend/screens/profile/settings_tab.dart create mode 100644 lib/frontend/widgets/custom_notification.dart diff --git a/lib/core/config/spoof_data.dart b/lib/core/config/spoof_data.dart new file mode 100644 index 0000000..f60c3ff --- /dev/null +++ b/lib/core/config/spoof_data.dart @@ -0,0 +1,31 @@ +class SpoofData { + static const List deviceNames = [ + 'Samsung Galaxy S23', + 'Xiaomi 13 Pro', + 'Google Pixel 7', + 'OnePlus 11', + ]; + + static const List osVersions = ['12', '13', '14']; + + static const List resolutions = [ + '1080x2400', + '1440x3200', + '720x1600', + ]; + + static const List deviceIds = [ + 'a1b2c3d4e5f6', + 'f8e7d6c5b4a3', + '9876543210ab', + '1234567890cd', + ]; + + static const List architectures = ['arm64-v8a', 'armeabi-v7a']; + + static const String deviceType = 'android'; + static const String timezone = 'Europe/Moscow'; + static const String locale = 'ru_RU'; + static const String appVersion = '26.10.1'; + static const String buildNumber = '6728'; +} diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index 44a8de4..7a705d7 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -7,85 +7,8 @@ import 'package:komet/core/config/countries.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'code_confirmation_screen.dart'; import 'select_country_screen.dart'; - -void _showCustomNotification(BuildContext context, String message) { - final overlay = Overlay.of(context); - final entry = OverlayEntry( - builder: (context) => _CustomNotification(message: message), - ); - overlay.insert(entry); - Future.delayed(const Duration(milliseconds: 1900), () { - entry.remove(); - }); -} - -class _CustomNotification extends StatefulWidget { - final String message; - const _CustomNotification({required this.message}); - - @override - State<_CustomNotification> createState() => _CustomNotificationState(); -} - -class _CustomNotificationState extends State<_CustomNotification> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _opacity; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - ); - _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller); - _controller.forward(); - Future.delayed(const Duration(milliseconds: 1600), () { - if (mounted) _controller.reverse(); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return Positioned( - bottom: 60, - left: 0, - right: 0, - child: Material( - color: Colors.transparent, - child: Center( - child: FadeTransition( - opacity: _opacity, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), - ), - child: Text( - widget.message, - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ), - ); - } -} +import 'spoff_redacted_screen.dart'; +import '../../widgets/custom_notification.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -482,12 +405,72 @@ class _LoginScreenState extends State { void _validateAndSubmit() { if (!_isTOSRead) { - _showCustomNotification(context, 'Соглащение прочитай щегол'); + showCustomNotification(context, 'Соглащение прочитай щегол'); return; } _showPhoneConfirmationDialog(_phoneController.text); } + void _showSecurityOptions(BuildContext context) { + final cs = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: cs.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Symbols.security, color: cs.onSurface), + title: Text( + 'Подделка спуфа', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SpoffRedactedScreen(), + ), + ); + }, + ), + ListTile( + leading: Icon(Symbols.vpn_lock, color: cs.onSurface), + title: Text( + 'Прокси', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + } + void _showOtherLoginMethods(BuildContext context) { final cs = Theme.of(context).colorScheme; showModalBottomSheet( @@ -588,7 +571,7 @@ class _LoginScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( - onPressed: () {}, + onPressed: () => _showSecurityOptions(context), icon: Icon( Symbols.admin_panel_settings, color: cs.onSurfaceVariant, diff --git a/lib/frontend/screens/auth/spoff_redacted_screen.dart b/lib/frontend/screens/auth/spoff_redacted_screen.dart new file mode 100644 index 0000000..a553a59 --- /dev/null +++ b/lib/frontend/screens/auth/spoff_redacted_screen.dart @@ -0,0 +1,217 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:komet/core/config/spoof_data.dart'; +import 'package:komet/frontend/widgets/custom_notification.dart'; + +class SpoffRedactedScreen extends StatefulWidget { + const SpoffRedactedScreen({super.key}); + + @override + State createState() => _SpoffRedactedScreenState(); +} + +class _SpoffRedactedScreenState extends State { + final TextEditingController _deviceNameController = TextEditingController(); + final TextEditingController _osVersionController = TextEditingController(); + final TextEditingController _resolutionController = TextEditingController(); + final TextEditingController _deviceIdController = TextEditingController(); + final TextEditingController _architectureController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + + final random = Random(); + + setState(() { + _deviceNameController.text = + prefs.getString('spoof_device_name') ?? + SpoofData.deviceNames[random.nextInt(SpoofData.deviceNames.length)]; + _osVersionController.text = + prefs.getString('spoof_os_version') ?? + SpoofData.osVersions[random.nextInt(SpoofData.osVersions.length)]; + _resolutionController.text = + prefs.getString('spoof_resolution') ?? + SpoofData.resolutions[random.nextInt(SpoofData.resolutions.length)]; + _deviceIdController.text = + prefs.getString('spoof_device_id') ?? + SpoofData.deviceIds[random.nextInt(SpoofData.deviceIds.length)]; + _architectureController.text = + prefs.getString('spoof_architecture') ?? + SpoofData.architectures[random.nextInt( + SpoofData.architectures.length, + )]; + }); + } + + Future _saveSettings() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('spoof_device_name', _deviceNameController.text); + await prefs.setString('spoof_os_version', _osVersionController.text); + await prefs.setString('spoof_resolution', _resolutionController.text); + await prefs.setString('spoof_device_id', _deviceIdController.text); + await prefs.setString('spoof_architecture', _architectureController.text); + + if (mounted) { + showCustomNotification(context, 'Настройки сохранены'); + } + } + + @override + void dispose() { + _deviceNameController.dispose(); + _osVersionController.dispose(); + _resolutionController.dispose(); + _deviceIdController.dispose(); + _architectureController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + iconTheme: IconThemeData(color: cs.onSurface), + title: Text( + 'Подделка спуфа', + style: GoogleFonts.inter( + color: cs.onSurface, + fontWeight: FontWeight.w500, + ), + ), + backgroundColor: cs.surface, + actions: [ + IconButton( + icon: Icon(Icons.check, color: cs.primary), + onPressed: _saveSettings, + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildTextField( + controller: TextEditingController(text: SpoofData.deviceType), + label: 'Тип устройства', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _deviceNameController, + label: 'Имя устройства', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _osVersionController, + label: 'Версия ОС', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _resolutionController, + label: 'Разрешение экрана', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.timezone), + label: 'Часовой пояс', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.locale), + label: 'Локаль', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _deviceIdController, + label: 'ID устройства', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.appVersion), + label: 'Версия приложения', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.buildNumber), + label: 'Build Number', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _architectureController, + label: 'Архитектура', + cs: cs, + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required ColorScheme cs, + bool readOnly = false, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.inter( + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + readOnly: readOnly, + style: GoogleFonts.inter( + color: readOnly ? cs.onSurfaceVariant : cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + filled: true, + fillColor: readOnly + ? cs.surfaceContainerHighest + : cs.surfaceContainerHigh, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + ), + ], + ); + } +} diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart new file mode 100644 index 0000000..21bf27b --- /dev/null +++ b/lib/frontend/screens/calls/calls_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class CallsTab extends StatelessWidget { + const CallsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Звонки (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 4260cd8..06326fc 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'dart:math'; import 'dart:ui' as ui; +import '../calls/calls_tab.dart'; +import '../contacts/contacts_tab.dart'; +import '../profile/settings_tab.dart'; + class ChatListScreen extends StatefulWidget { const ChatListScreen({super.key}); @@ -9,9 +14,38 @@ class ChatListScreen extends StatefulWidget { State createState() => _ChatListScreenState(); } -class _ChatListScreenState extends State { +class _ChatListScreenState extends State + with SingleTickerProviderStateMixin { String _selectedCategory = 'Все чаты'; int _currentNavIndex = 0; + bool _isFabOpen = false; + late AnimationController _fabController; + + @override + void initState() { + super.initState(); + _fabController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 350), + ); + } + + @override + void dispose() { + _fabController.dispose(); + super.dispose(); + } + + void _toggleFab() { + setState(() { + _isFabOpen = !_isFabOpen; + if (_isFabOpen) { + _fabController.forward(); + } else { + _fabController.reverse(); + } + }); + } @override Widget build(BuildContext context) { @@ -22,250 +56,375 @@ class _ChatListScreenState extends State { bottom: false, child: Stack( children: [ - CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), - sliver: SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Подключение...', - style: TextStyle( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), - ), - IconButton( - onPressed: () {}, - icon: Icon( - Symbols.more_vert, - color: cs.outline, - weight: 400, - ), - ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 96, - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20), - children: [ - _buildStoryItem('Даша', 'https://i.pravatar.cc/150?u=dasha', true), - _buildStoryItem('Мастика', 'https://i.pravatar.cc/150?u=mastika', false), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - sliver: SliverToBoxAdapter( - child: Container( - height: 44, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Icon(Symbols.search, color: cs.outline, size: 20, weight: 400), - const SizedBox(width: 10), - Expanded( - child: TextField( - style: TextStyle(color: cs.onSurface, fontSize: 15), - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle(color: cs.outline, fontSize: 15), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, + IndexedStack( + index: _currentNavIndex, + children: [ + CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Подключение...', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', ), ), + PopupMenuButton( + icon: Icon( + Symbols.more_vert, + color: cs.outline, + weight: 400, + ), + offset: const Offset(0, 48), + elevation: 4, + color: cs.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + itemBuilder: (context) => [ + _buildPopupMenuItem( + 1, + 'Кнопка 1', + Symbols.settings, + ), + _buildPopupMenuItem( + 2, + 'Кнопка 2', + Symbols.notifications, + ), + _buildPopupMenuItem( + 3, + 'Кнопка 3', + Symbols.shield, + ), + _buildPopupMenuItem( + 4, + 'Кнопка 4', + Symbols.info, + ), + ], + onSelected: (value) { + // Action handler + }, + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 96, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + _buildStoryItem( + 'Даша', + 'https://i.pravatar.cc/150?u=dasha', + true, + ), + _buildStoryItem( + 'Мастика', + 'https://i.pravatar.cc/150?u=mastika', + false, + ), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + sliver: SliverToBoxAdapter( + child: Container( + height: 44, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), ), - ], + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon( + Symbols.search, + color: cs.outline, + size: 20, + weight: 400, + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ), ), ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 48, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - ui.PointerDeviceKind.touch, - ui.PointerDeviceKind.mouse, - ui.PointerDeviceKind.trackpad, - }, - ), - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - physics: const BouncingScrollPhysics(), - children: [ - _buildFolderChip('Все чаты'), - const SizedBox(width: 8), - _buildFolderChip('Контакты'), - const SizedBox(width: 8), - _buildFolderChip('Пидоры'), - const SizedBox(width: 8), - _buildFolderChip('Каналы'), - const SizedBox(width: 8), - _buildFolderChip('Группы'), - const SizedBox(width: 8), - _buildFolderChip('Боты'), - const SizedBox(width: 8), - _buildFolderChip('Избранное'), - const SizedBox(width: 8), - _buildFolderChip('Архив'), - ], + SliverToBoxAdapter( + child: SizedBox( + height: 48, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + ui.PointerDeviceKind.trackpad, + }, + ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + physics: const BouncingScrollPhysics(), + children: [ + _buildFolderChip('Все чаты'), + const SizedBox(width: 8), + _buildFolderChip('Контакты'), + const SizedBox(width: 8), + _buildFolderChip('Пидоры'), + const SizedBox(width: 8), + _buildFolderChip('Каналы'), + const SizedBox(width: 8), + _buildFolderChip('Группы'), + const SizedBox(width: 8), + _buildFolderChip('Боты'), + const SizedBox(width: 8), + _buildFolderChip('Избранное'), + const SizedBox(width: 8), + _buildFolderChip('Архив'), + ], + ), + ), ), ), - ), - ), - SliverList( - delegate: SliverChildListDelegate([ - _buildChatItem( - 'Станислав', - 'Хорошо', - '10:07', - 'https://i.pravatar.cc/150?u=stas', - isOnline: true, - isRead: true, + SliverList( + delegate: SliverChildListDelegate([ + _buildChatItem( + 'Станислав', + 'Хорошо', + '10:07', + 'https://i.pravatar.cc/150?u=stas', + isOnline: true, + isRead: true, + ), + _buildChatItem( + 'Илья', + 'печатает...', + '10:07', + 'https://i.pravatar.cc/150?u=ilya', + isOnline: true, + isTyping: true, + unreadCount: 1, + ), + _buildChatItem( + 'Вероника', + 'Спасибо', + '09:56', + 'https://i.pravatar.cc/150?u=veronika', + isRead: true, + ), + _buildChatItem( + 'Komet Client', + 'Кстати. Смотрите, какую шту...', + '09:56', + 'https://i.pravatar.cc/150?u=komet', + unreadCount: 5, + isMuted: true, + ), + _buildChatItem( + '4-й подъезд', + 'Людмила: Сколько?', + '09:34', + 'https://i.pravatar.cc/150?u=podezd', + unreadCount: 78, + isMuted: true, + ), + const SizedBox(height: 100), + ]), ), - _buildChatItem( - 'Илья', - 'печатает...', - '10:07', - 'https://i.pravatar.cc/150?u=ilya', - isOnline: true, - isTyping: true, - unreadCount: 1, - ), - _buildChatItem( - 'Вероника', - 'Спасибо', - '09:56', - 'https://i.pravatar.cc/150?u=veronika', - isRead: true, - ), - _buildChatItem( - 'Komet Client', - 'Кстати. Смотрите, какую шту...', - '09:56', - 'https://i.pravatar.cc/150?u=komet', - unreadCount: 5, - isMuted: true, - ), - _buildChatItem( - '4-й подъезд', - 'Людмила: Сколько?', - '09:34', - 'https://i.pravatar.cc/150?u=podezd', - unreadCount: 78, - isMuted: true, - ), - const SizedBox(height: 100), - ]), + ], ), + const CallsTab(), + const ContactsTab(), + const SettingsTab(), ], ), Positioned( left: 8, right: 8, - bottom: 24, - child: Container( - height: 68, - padding: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(34), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: LayoutBuilder( - builder: (context, constraints) { - double totalWidth = constraints.maxWidth; + bottom: 24.0, + child: RepaintBoundary( + child: Container( + height: 68, + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(34), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + double totalWidth = constraints.maxWidth; - double totalWeight = 5.2; - double unitWidth = totalWidth / totalWeight; - double activeWidth = unitWidth * 2.2; - double inactiveWidth = unitWidth * 1.0; + double totalWeight = 5.2; + double unitWidth = totalWidth / totalWeight; + double activeWidth = unitWidth * 2.2; + double inactiveWidth = unitWidth * 1.0; - double leftOffset = 0; - for (int i = 0; i < _currentNavIndex; i++) { - leftOffset += inactiveWidth; - } + double leftOffset = 0; + for (int i = 0; i < _currentNavIndex; i++) { + leftOffset += inactiveWidth; + } - return Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 350), - curve: Curves.easeOutCubic, - left: leftOffset + 4, - top: 8, - bottom: 8, - width: activeWidth - 8, - child: Container( - decoration: BoxDecoration( - color: cs.primary, - borderRadius: BorderRadius.circular(26), + return Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + left: leftOffset + 4, + top: 8, + bottom: 8, + width: activeWidth - 8, + child: Container( + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(26), + ), ), ), - ), - Row( - children: List.generate(4, (index) { - IconData icon; - String label; - switch (index) { - case 0: icon = Symbols.chat_bubble; label = 'Чаты'; break; - case 1: icon = Symbols.call; label = 'Звонки'; break; - case 2: icon = Symbols.person_pin; label = 'Контакты'; break; - default: icon = Symbols.settings; label = 'Настройки'; - } + Row( + children: List.generate(4, (index) { + IconData icon; + String label; + switch (index) { + case 0: + icon = Symbols.chat_bubble; + label = 'Чаты'; + break; + case 1: + icon = Symbols.call; + label = 'Звонки'; + break; + case 2: + icon = Symbols.person_pin; + label = 'Контакты'; + break; + default: + icon = Symbols.settings; + label = 'Настройки'; + } - bool isSelected = _currentNavIndex == index; - return AnimatedContainer( - duration: const Duration(milliseconds: 350), - curve: Curves.easeOutCubic, - width: isSelected ? (activeWidth - 0.5) : (inactiveWidth - 0.5), - child: ClipRRect( - borderRadius: BorderRadius.circular(26), - child: _buildNavItem(index, icon, label), - ), - ); - }), - ), - ], - ); - }, + bool isSelected = _currentNavIndex == index; + return AnimatedContainer( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + width: isSelected + ? (activeWidth - 0.5) + : (inactiveWidth - 0.5), + child: ClipRRect( + borderRadius: BorderRadius.circular(26), + child: _buildNavItem(index, icon, label), + ), + ); + }), + ), + ], + ); + }, + ), ), ), ), - Positioned( - right: 20, - bottom: 110, - child: FloatingActionButton( - onPressed: () {}, - backgroundColor: cs.primaryContainer, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Icon(Symbols.add, color: cs.onPrimaryContainer, size: 28, weight: 400), - ), + AnimatedBuilder( + animation: _fabController, + builder: (context, child) { + final double val = Curves.easeOutCubic.transform( + _fabController.value, + ); + return Stack( + clipBehavior: Clip.none, + children: [ + if (_fabController.value > 0) + Positioned.fill( + child: GestureDetector( + onTap: _toggleFab, + behavior: HitTestBehavior.opaque, + child: Container( + color: Colors.black.withValues(alpha: val * 0.2), + ), + ), + ), + if (_fabController.value > 0) + Positioned( + right: 20, + bottom: 110 + 74, + child: RepaintBoundary( + child: Transform.scale( + scale: val, + alignment: Alignment.bottomRight, + child: Opacity( + opacity: val > 0.5 ? (val - 0.5) * 2 : 0, + child: _buildFabMenu(), + ), + ), + ), + ), + Positioned( + right: 20, + bottom: 110, + child: FloatingActionButton( + onPressed: _toggleFab, + backgroundColor: cs.primaryContainer, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Transform.rotate( + angle: val * (pi / 4), + child: Icon( + Symbols.add, + color: cs.onPrimaryContainer, + size: 28, + weight: 400, + ), + ), + ), + ), + ], + ); + }, ), ], ), @@ -345,10 +504,7 @@ class _ChatListScreenState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), leading: Stack( children: [ - CircleAvatar( - radius: 24, - backgroundImage: NetworkImage(imageUrl), - ), + CircleAvatar(radius: 24, backgroundImage: NetworkImage(imageUrl)), if (isOnline) Positioned( right: 0, @@ -378,15 +534,14 @@ class _ChatListScreenState extends State { ), ), if (isMuted) - Icon(Symbols.notifications_off, color: cs.outlineVariant, size: 14, weight: 400), - const SizedBox(width: 8), - Text( - time, - style: TextStyle( - color: cs.outline, - fontSize: 12, + Icon( + Symbols.notifications_off, + color: cs.outlineVariant, + size: 14, + weight: 400, ), - ), + const SizedBox(width: 8), + Text(time, style: TextStyle(color: cs.outline, fontSize: 12)), ], ), subtitle: Padding( @@ -409,7 +564,9 @@ class _ChatListScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isMuted ? cs.surfaceContainerHighest : cs.surfaceContainerHigh, + color: isMuted + ? cs.surfaceContainerHighest + : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(10), ), child: Text( @@ -478,4 +635,83 @@ class _ChatListScreenState extends State { ), ); } + + Widget _buildFabMenu() { + final cs = Theme.of(context).colorScheme; + return Container( + width: 220, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFabMenuItem(Symbols.search, 'Найти по номеру'), + _buildFabMenuItem(Symbols.group_add, 'Добавить группу'), + _buildFabMenuItem(Symbols.campaign, 'Создать канал'), + ], + ), + ); + } + + Widget _buildFabMenuItem(IconData icon, String title) { + final cs = Theme.of(context).colorScheme; + return InkWell( + onTap: () { + // Action logic here + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 22), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + PopupMenuItem _buildPopupMenuItem( + int value, + String title, + IconData icon, + ) { + final cs = Theme.of(context).colorScheme; + return PopupMenuItem( + value: value, + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 20), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } } diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart new file mode 100644 index 0000000..c47b562 --- /dev/null +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ContactsTab extends StatelessWidget { + const ContactsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Контакты (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart new file mode 100644 index 0000000..2a60054 --- /dev/null +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class SettingsTab extends StatelessWidget { + const SettingsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Настройки (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/widgets/custom_notification.dart b/lib/frontend/widgets/custom_notification.dart new file mode 100644 index 0000000..85c6e91 --- /dev/null +++ b/lib/frontend/widgets/custom_notification.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +void showCustomNotification(BuildContext context, String message) { + final overlay = Overlay.of(context); + final entry = OverlayEntry( + builder: (context) => CustomNotification(message: message), + ); + overlay.insert(entry); + Future.delayed(const Duration(milliseconds: 1900), () { + entry.remove(); + }); +} + +class CustomNotification extends StatefulWidget { + final String message; + const CustomNotification({required this.message, super.key}); + + @override + State createState() => _CustomNotificationState(); +} + +class _CustomNotificationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + ); + _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller); + _controller.forward(); + Future.delayed(const Duration(milliseconds: 1600), () { + if (mounted) _controller.reverse(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Positioned( + bottom: 60, + left: 0, + right: 0, + child: Material( + color: Colors.transparent, + child: Center( + child: FadeTransition( + opacity: _opacity, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + widget.message, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } +} From f9f2efedf4106ee40d178366c6e1d8b2b220c5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Thu, 26 Mar 2026 18:05:30 +0700 Subject: [PATCH 8/9] =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/chats/chat_list_screen.dart | 872 ++++++++++++------ lib/frontend/screens/chats/chat_screen.dart | 240 +++++ 2 files changed, 810 insertions(+), 302 deletions(-) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 06326fc..678ac56 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'chat_screen.dart'; import '../calls/calls_tab.dart'; import '../contacts/contacts_tab.dart'; @@ -20,6 +23,27 @@ class _ChatListScreenState extends State int _currentNavIndex = 0; bool _isFabOpen = false; late AnimationController _fabController; + final Set _selectedChats = {}; + final ScrollController _scrollController = ScrollController(); + double _pullRatio = 0.0; // 0.0 = folded (hidden row), 1.0 = fully expanded + + bool get _isSelectionMode => _selectedChats.isNotEmpty; + + void _toggleSelection(String chatId) { + setState(() { + if (_selectedChats.contains(chatId)) { + _selectedChats.remove(chatId); + } else { + _selectedChats.add(chatId); + } + }); + } + + void _clearSelection() { + setState(() { + _selectedChats.clear(); + }); + } @override void initState() { @@ -28,11 +52,31 @@ class _ChatListScreenState extends State vsync: this, duration: const Duration(milliseconds: 350), ); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.hasClients) { + final double offset = _scrollController.offset; + if (offset < 0) { + final newRatio = (offset.abs() / 80.0).clamp(0.0, 1.0); + if (newRatio != _pullRatio) { + setState(() { + _pullRatio = newRatio; + }); + } + } else if (_pullRatio > 0) { + setState(() { + _pullRatio = 0.0; + }); + } + } } @override void dispose() { _fabController.dispose(); + _scrollController.dispose(); super.dispose(); } @@ -59,226 +103,328 @@ class _ChatListScreenState extends State IndexedStack( index: _currentNavIndex, children: [ - CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), - sliver: SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Подключение...', - style: TextStyle( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), + Listener( + onPointerSignal: (pointerSignal) { + if (pointerSignal is PointerScrollEvent) { + if (_scrollController.hasClients && + _scrollController.offset <= 0) { + if (pointerSignal.scrollDelta.dy < 0) { + // Scrolled UP (pulling down) + setState(() { + _pullRatio = (_pullRatio + 0.2).clamp(0.0, 1.0); + }); + } else if (pointerSignal.scrollDelta.dy > 0 && + _pullRatio > 0) { + // Scrolled DOWN (folding up) + setState(() { + _pullRatio = (_pullRatio - 0.2).clamp(0.0, 1.0); + }); + } + } + } + }, + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: [ + SliverToBoxAdapter( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + height: _isSelectionMode + ? 0 + : (132 + (96 * _pullRatio)), + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + transform: Matrix4.translationValues( + 0, + _isSelectionMode ? -100 : 0, + 0, ), - PopupMenuButton( - icon: Icon( - Symbols.more_vert, - color: cs.outline, - weight: 400, - ), - offset: const Offset(0, 48), - elevation: 4, - color: cs.surfaceContainerHigh, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - itemBuilder: (context) => [ - _buildPopupMenuItem( - 1, - 'Кнопка 1', - Symbols.settings, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + 20, + 12, + 20, + 4, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (_pullRatio < 0.8) + Opacity( + opacity: 1.0 - _pullRatio, + child: Container( + width: 50 * (1.0 - _pullRatio), + height: 32, + margin: const EdgeInsets.only( + right: 8, + ), + child: Stack( + children: [ + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=dasha', + 0, + ), + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=mastika', + 1, + ), + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=stas', + 2, + ), + ], + ), + ), + ), + Text( + 'Подключение...', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + ], + ), + PopupMenuButton( + icon: Icon( + Symbols.more_vert, + color: cs.outline, + weight: 400, + ), + offset: const Offset(0, 48), + elevation: 4, + color: cs.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 16, + ), + ), + itemBuilder: (context) => [ + _buildPopupMenuItem( + 1, + 'Кнопка 1', + Symbols.settings, + ), + _buildPopupMenuItem( + 2, + 'Кнопка 2', + Symbols.notifications, + ), + _buildPopupMenuItem( + 3, + 'Кнопка 3', + Symbols.shield, + ), + _buildPopupMenuItem( + 4, + 'Кнопка 4', + Symbols.info, + ), + ], + ), + ], + ), ), - _buildPopupMenuItem( - 2, - 'Кнопка 2', - Symbols.notifications, + SizedBox( + height: 96 * _pullRatio, + child: Opacity( + opacity: _pullRatio, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + children: [ + _buildStoryItem( + 'Даша', + 'https://i.pravatar.cc/150?u=dasha', + true, + ), + _buildStoryItem( + 'Мастика', + 'https://i.pravatar.cc/150?u=mastika', + false, + ), + ], + ), + ), ), - _buildPopupMenuItem( - 3, - 'Кнопка 3', - Symbols.shield, - ), - _buildPopupMenuItem( - 4, - 'Кнопка 4', - Symbols.info, + Padding( + padding: const EdgeInsets.fromLTRB( + 20, + 4, + 20, + 12, + ), + child: Container( + height: 44, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Icon( + Symbols.search, + color: cs.outline, + size: 20, + weight: 400, + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ), ), ], - onSelected: (value) { - // Action handler - }, ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 96, - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20), - children: [ - _buildStoryItem( - 'Даша', - 'https://i.pravatar.cc/150?u=dasha', - true, - ), - _buildStoryItem( - 'Мастика', - 'https://i.pravatar.cc/150?u=mastika', - false, - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - sliver: SliverToBoxAdapter( - child: Container( - height: 44, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Icon( - Symbols.search, - color: cs.outline, - size: 20, - weight: 400, - ), - const SizedBox(width: 10), - Expanded( - child: TextField( - style: TextStyle( - color: cs.onSurface, - fontSize: 15, - ), - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle( - color: cs.outline, - fontSize: 15, - ), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, + ), + ), + SliverPadding( + padding: EdgeInsets.only( + top: _isSelectionMode ? 64 : 0, + ), + sliver: SliverToBoxAdapter( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + height: 48, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + ui.PointerDeviceKind.trackpad, + }, ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, ), + physics: const BouncingScrollPhysics(), + children: [ + _buildFolderChip('Все чаты'), + const SizedBox(width: 8), + _buildFolderChip('Контакты'), + const SizedBox(width: 8), + _buildFolderChip('Пидоры'), + const SizedBox(width: 8), + _buildFolderChip('Каналы'), + const SizedBox(width: 8), + _buildFolderChip('Группы'), + const SizedBox(width: 8), + _buildFolderChip('Боты'), + const SizedBox(width: 8), + _buildFolderChip('Избранное'), + const SizedBox(width: 8), + _buildFolderChip('Архив'), + ], ), - ], - ), - ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 48, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - ui.PointerDeviceKind.touch, - ui.PointerDeviceKind.mouse, - ui.PointerDeviceKind.trackpad, - }, - ), - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, ), - physics: const BouncingScrollPhysics(), - children: [ - _buildFolderChip('Все чаты'), - const SizedBox(width: 8), - _buildFolderChip('Контакты'), - const SizedBox(width: 8), - _buildFolderChip('Пидоры'), - const SizedBox(width: 8), - _buildFolderChip('Каналы'), - const SizedBox(width: 8), - _buildFolderChip('Группы'), - const SizedBox(width: 8), - _buildFolderChip('Боты'), - const SizedBox(width: 8), - _buildFolderChip('Избранное'), - const SizedBox(width: 8), - _buildFolderChip('Архив'), - ], ), ), ), - ), - SliverList( - delegate: SliverChildListDelegate([ - _buildChatItem( - 'Станислав', - 'Хорошо', - '10:07', - 'https://i.pravatar.cc/150?u=stas', - isOnline: true, - isRead: true, - ), - _buildChatItem( - 'Илья', - 'печатает...', - '10:07', - 'https://i.pravatar.cc/150?u=ilya', - isOnline: true, - isTyping: true, - unreadCount: 1, - ), - _buildChatItem( - 'Вероника', - 'Спасибо', - '09:56', - 'https://i.pravatar.cc/150?u=veronika', - isRead: true, - ), - _buildChatItem( - 'Komet Client', - 'Кстати. Смотрите, какую шту...', - '09:56', - 'https://i.pravatar.cc/150?u=komet', - unreadCount: 5, - isMuted: true, - ), - _buildChatItem( - '4-й подъезд', - 'Людмила: Сколько?', - '09:34', - 'https://i.pravatar.cc/150?u=podezd', - unreadCount: 78, - isMuted: true, - ), - const SizedBox(height: 100), - ]), - ), - ], - ), + SliverList( + delegate: SliverChildListDelegate([ + _buildChatItem( + 'stas', + 'Станислав', + 'Хорошо', + '10:07', + 'https://i.pravatar.cc/150?u=stas', + isOnline: true, + isRead: true, + ), + _buildChatItem( + 'ilya', + 'Илья', + 'печатает...', + '10:07', + 'https://i.pravatar.cc/150?u=ilya', + isOnline: true, + isTyping: true, + unreadCount: 1, + ), + _buildChatItem( + 'veronika', + 'Вероника', + 'Спасибо', + '09:56', + 'https://i.pravatar.cc/150?u=veronika', + isRead: true, + ), + _buildChatItem( + 'komet', + 'Komet Client', + 'Кстати. Смотрите, какую шту...', + '09:56', + 'https://i.pravatar.cc/150?u=komet', + unreadCount: 5, + isMuted: true, + ), + _buildChatItem( + 'podezd', + '4-й подъезд', + 'Людмила: Сколько?', + '09:34', + 'https://i.pravatar.cc/150?u=podezd', + unreadCount: 78, + isMuted: true, + ), + ]), + ), + ], + ), // CustomScrollView + ), // Listener const CallsTab(), const ContactsTab(), const SettingsTab(), ], ), - Positioned( + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, left: 8, right: 8, - bottom: 24.0, + bottom: _isSelectionMode ? -100 : 24.0, child: RepaintBoundary( child: Container( height: 68, @@ -386,46 +532,99 @@ class _ChatListScreenState extends State ), ), ), - if (_fabController.value > 0) + if (!_isSelectionMode) ...[ + if (_fabController.value > 0) + Positioned( + right: 20, + bottom: 110 + 74, + child: RepaintBoundary( + child: Transform.scale( + scale: val, + alignment: Alignment.bottomRight, + child: Opacity( + opacity: val > 0.5 ? (val - 0.5) * 2 : 0, + child: _buildFabMenu(), + ), + ), + ), + ), Positioned( right: 20, - bottom: 110 + 74, - child: RepaintBoundary( - child: Transform.scale( - scale: val, - alignment: Alignment.bottomRight, - child: Opacity( - opacity: val > 0.5 ? (val - 0.5) * 2 : 0, - child: _buildFabMenu(), + bottom: 110, + child: FloatingActionButton( + onPressed: _toggleFab, + backgroundColor: cs.primaryContainer, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Transform.rotate( + angle: val * (pi / 4), + child: Icon( + Symbols.add, + color: cs.onPrimaryContainer, + size: 28, + weight: 400, ), ), ), ), - Positioned( - right: 20, - bottom: 110, - child: FloatingActionButton( - onPressed: _toggleFab, - backgroundColor: cs.primaryContainer, - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Transform.rotate( - angle: val * (pi / 4), - child: Icon( - Symbols.add, - color: cs.onPrimaryContainer, - size: 28, - weight: 400, - ), - ), - ), - ), + ], ], ); }, ), + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + top: _isSelectionMode ? 0 : -80, + left: 0, + right: 0, + child: Container( + height: 64, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: cs.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + IconButton( + icon: Icon(Symbols.arrow_back, color: cs.onSurface), + onPressed: _clearSelection, + ), + const SizedBox(width: 8), + Text( + _selectedChats.length.toString(), + style: TextStyle( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + IconButton( + icon: Icon(Symbols.delete, color: cs.onSurface), + onPressed: () {}, + ), + IconButton( + icon: Icon(Symbols.archive, color: cs.onSurface), + onPressed: () {}, + ), + IconButton( + icon: Icon(Symbols.volume_off, color: cs.onSurface), + onPressed: () {}, + ), + ], + ), + ), + ), ], ), ), @@ -489,6 +688,7 @@ class _ChatListScreenState extends State } Widget _buildChatItem( + String id, String name, String message, String time, @@ -500,87 +700,139 @@ class _ChatListScreenState extends State bool isMuted = false, }) { final cs = Theme.of(context).colorScheme; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), - leading: Stack( - children: [ - CircleAvatar(radius: 24, backgroundImage: NetworkImage(imageUrl)), - if (isOnline) - Positioned( - right: 0, - bottom: 0, - child: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: cs.primary, - shape: BoxShape.circle, - border: Border.all(color: cs.surface, width: 2), - ), - ), - ), - ], - ), - title: Row( - children: [ - Expanded( - child: Text( - name, - style: TextStyle( - color: cs.onSurface, - fontSize: 15, - fontWeight: FontWeight.w600, - ), + final isSelected = _selectedChats.contains(id); + + return InkWell( + onTap: () { + if (_isSelectionMode) { + _toggleSelection(id); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatScreen(name: name, imageUrl: imageUrl), ), + ); + } + }, + onLongPress: () => _toggleSelection(id), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + color: isSelected ? cs.primary.withOpacity(0.08) : Colors.transparent, + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 2, ), - if (isMuted) - Icon( - Symbols.notifications_off, - color: cs.outlineVariant, - size: 14, - weight: 400, - ), - const SizedBox(width: 8), - Text(time, style: TextStyle(color: cs.outline, fontSize: 12)), - ], - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - children: [ - Expanded( - child: Text( - message, - style: TextStyle( - color: isTyping ? cs.primary : cs.outline, - fontSize: 14, - fontWeight: isTyping ? FontWeight.w500 : FontWeight.w400, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (unreadCount > 0) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: isMuted - ? cs.surfaceContainerHighest - : cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(10), + leading: Stack( + children: [ + CircleAvatar(radius: 24, backgroundImage: NetworkImage(imageUrl)), + if (isSelected) + Positioned( + right: -2, + bottom: -2, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: cs.primary, + shape: BoxShape.circle, + border: Border.all(color: cs.surface, width: 2), + ), + child: Icon( + Symbols.check, + color: cs.onPrimary, + size: 14, + weight: 600, + ), + ), + ) + else if (isOnline) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: cs.primary, + shape: BoxShape.circle, + border: Border.all(color: cs.surface, width: 2), + ), + ), ), + ], + ), + title: Row( + children: [ + Expanded( child: Text( - unreadCount.toString(), + name, style: TextStyle( - color: isMuted ? cs.outline : cs.onSurface, - fontSize: 11, + color: cs.onSurface, + fontSize: 15, fontWeight: FontWeight.w600, ), ), - ) - else if (isRead) - Icon(Symbols.done_all, color: cs.primary, size: 16, weight: 400), - ], + ), + if (isMuted) + Icon( + Symbols.notifications_off, + color: cs.outlineVariant, + size: 14, + weight: 400, + ), + const SizedBox(width: 8), + Text(time, style: TextStyle(color: cs.outline, fontSize: 12)), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Expanded( + child: Text( + message, + style: TextStyle( + color: isTyping ? cs.primary : cs.outline, + fontSize: 14, + fontWeight: isTyping ? FontWeight.w500 : FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (unreadCount > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: isMuted + ? cs.surfaceContainerHighest + : cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + unreadCount.toString(), + style: TextStyle( + color: isMuted ? cs.outline : cs.onSurface, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ) + else if (isRead) + Icon( + Symbols.done_all, + color: cs.primary, + size: 16, + weight: 400, + ), + ], + ), + ), ), ), ); @@ -714,4 +966,20 @@ class _ChatListScreenState extends State ), ); } + + Widget _buildFoldedStory(String imageUrl, int index) { + return Positioned( + left: index * 12.0, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 2), + ), + child: CircleAvatar( + radius: 12, + backgroundImage: NetworkImage(imageUrl), + ), + ), + ); + } } diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index e69de29..50f586d 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'dart:ui'; + +class ChatScreen extends StatefulWidget { + final String name; + final String imageUrl; + + const ChatScreen({super.key, required this.name, required this.imageUrl}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + final TextEditingController _messageController = TextEditingController(); + bool _hasText = false; + + @override + void initState() { + super.initState(); + _messageController.addListener(_onTextChanged); + } + + @override + void dispose() { + _messageController.removeListener(_onTextChanged); + _messageController.dispose(); + super.dispose(); + } + + void _onTextChanged() { + final bool newHasText = _messageController.text.trim().isNotEmpty; + if (newHasText != _hasText) { + setState(() { + _hasText = newHasText; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: const Color(0xFF1B1B1B).withOpacity(0.8), + elevation: 0, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon( + Symbols.arrow_back, + color: Colors.white, + weight: 400, + ), + onPressed: () => Navigator.pop(context), + ), + titleSpacing: 0, + title: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundImage: NetworkImage(widget.imageUrl), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.name, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + ], + ), + const Text( + 'Connecting...', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Symbols.call, color: Colors.white, weight: 400), + onPressed: () {}, + ), + IconButton( + icon: const Icon( + Symbols.more_vert, + color: Colors.white, + weight: 400, + ), + onPressed: () {}, + ), + ], + ), + body: Stack( + children: [ + Positioned.fill( + child: Image.network( + 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809', + fit: BoxFit.cover, + opacity: const AlwaysStoppedAnimation(0.4), + ), + ), + const Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black87], + ), + ), + ), + ), + Column(children: [const Spacer(), _buildInputArea(context)]), + ], + ), + ); + } + + Widget _buildInputArea(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + constraints: const BoxConstraints( + minHeight: 54, + maxHeight: 180, + ), + decoration: BoxDecoration( + color: const Color(0xFF1B1B1B).withOpacity(0.9), + borderRadius: BorderRadius.circular(28), + border: Border.all( + color: Colors.white.withOpacity(0.1), + width: 0.5, + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Symbols.face, + color: Colors.white.withOpacity(0.7), + size: 24, + weight: 400, + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _messageController, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + maxLines: null, + keyboardType: TextInputType.multiline, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + hintText: 'Message', + hintStyle: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: _hasText ? 0 : 36, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: _hasText ? 0 : 1, + child: _hasText + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(left: 12), + child: Icon( + Symbols.attachment, + color: Colors.white.withOpacity(0.7), + size: 24, + weight: 400, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Container( + width: 54, + height: 54, + alignment: Alignment.center, + decoration: const BoxDecoration( + color: Color(0xFF2B2B2B), + shape: BoxShape.circle, + ), + child: Icon( + _hasText ? Symbols.send : Symbols.mic, + color: Colors.white, + size: 24, + weight: 400, + ), + ), + ], + ), + ), + ); + } +} From 7fcf36b71a3a30227c944e6c2d88a15a64559909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Thu, 26 Mar 2026 19:36:19 +0700 Subject: [PATCH 9/9] =?UTF-8?q?=D0=AD=D0=BA=D1=80=D0=B0=D0=BD=20=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/profile/settings_tab.dart | 217 +++++++++++++++++- 1 file changed, 209 insertions(+), 8 deletions(-) diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 2a60054..95e1f6a 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:material_symbols_icons/symbols.dart'; class SettingsTab extends StatelessWidget { const SettingsTab({super.key}); @@ -7,15 +7,216 @@ class SettingsTab extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Center( - child: Text( - 'Настройки (Заглушка)', - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.w500, + return Scaffold( + backgroundColor: cs.surface, + body: SafeArea( + bottom: false, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter(child: _buildHeader(context, cs)), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: _buildSection( + context, + cs, + items: const [ + _SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'), + _SettingsItem( + icon: Symbols.language, + label: 'Войти в сферум', + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: _buildSection( + context, + cs, + items: const [ + _SettingsItem( + icon: Symbols.notifications_active, + label: 'Уведомления и звук', + ), + _SettingsItem(icon: Symbols.lock, label: 'Безопасность'), + _SettingsItem(icon: Symbols.devices, label: 'Устройства'), + ], + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 120)), + ], ), ), ); } + + Widget _buildHeader(BuildContext context, ColorScheme cs) { + return Padding( + padding: const EdgeInsets.fromLTRB(8, 12, 8, 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Symbols.qr_code_2, + color: cs.onSurfaceVariant, + size: 26, + weight: 400, + ), + onPressed: () {}, + ), + IconButton( + icon: Icon( + Symbols.edit, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 8), + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: cs.primary.withValues(alpha: 0.5), + width: 2.5, + ), + ), + child: ClipOval( + child: Image.network( + 'https://i.pravatar.cc/150?u=ilya', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => CircleAvatar( + backgroundColor: cs.primaryContainer, + child: Icon( + Symbols.person, + color: cs.onPrimaryContainer, + size: 40, + ), + ), + ), + ), + ), + const SizedBox(height: 14), + Text( + 'Илья Беларуских', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Outfit', + ), + ), + const SizedBox(height: 3), + Text( + '@everrnyan', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } + + Widget _buildSection( + BuildContext context, + ColorScheme cs, { + required List<_SettingsItem> items, + }) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: List.generate(items.length, (index) { + final item = items[index]; + final isLast = index == items.length - 1; + return _buildSettingsRow(context, cs, item, isLast: isLast); + }), + ), + ), + ); + } + + Widget _buildSettingsRow( + BuildContext context, + ColorScheme cs, + _SettingsItem item, { + bool isLast = false, + }) { + return Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), + child: Row( + children: [ + Icon( + item.icon, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + item.label, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + Icon( + Symbols.chevron_right, + color: cs.outline, + size: 20, + weight: 400, + ), + ], + ), + ), + ), + ), + if (!isLast) + Padding( + padding: const EdgeInsets.only(left: 58), + child: Divider( + height: 1, + thickness: 1, + color: cs.outlineVariant.withValues(alpha: 0.35), + ), + ), + ], + ); + } +} + +class _SettingsItem { + final IconData icon; + final String label; + + const _SettingsItem({required this.icon, required this.label}); }