Merge branch 'dev/0.5.0' into feature/server-connection-handshake
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
kotlin.incremental=false
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,31 @@
|
||||
class SpoofData {
|
||||
static const List<String> deviceNames = [
|
||||
'Samsung Galaxy S23',
|
||||
'Xiaomi 13 Pro',
|
||||
'Google Pixel 7',
|
||||
'OnePlus 11',
|
||||
];
|
||||
|
||||
static const List<String> osVersions = ['12', '13', '14'];
|
||||
|
||||
static const List<String> resolutions = [
|
||||
'1080x2400',
|
||||
'1440x3200',
|
||||
'720x1600',
|
||||
];
|
||||
|
||||
static const List<String> deviceIds = [
|
||||
'a1b2c3d4e5f6',
|
||||
'f8e7d6c5b4a3',
|
||||
'9876543210ab',
|
||||
'1234567890cd',
|
||||
];
|
||||
|
||||
static const List<String> 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';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:logger/logger.dart'; //нет это не опасный вредоносный вирус логер который украдёт ваш аккаунт Browl Starz
|
||||
|
||||
final logger = Logger(
|
||||
// устанавливает минимальный уровень логов.
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../chats/chat_list_screen.dart';
|
||||
|
||||
class CodeConfirmationScreen extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
|
||||
const CodeConfirmationScreen({super.key, required this.phoneNumber});
|
||||
|
||||
@override
|
||||
State<CodeConfirmationScreen> createState() => _CodeConfirmationScreenState();
|
||||
}
|
||||
|
||||
class _CodeConfirmationScreenState extends State<CodeConfirmationScreen> {
|
||||
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();
|
||||
print('Resending code to ${widget.phoneNumber}');
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToChats() {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const ChatListScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant),
|
||||
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: GoogleFonts.inter(
|
||||
color: cs.onSurface,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Мы отправили SMS с кодом подтверждения на ваш номер телефона.',
|
||||
style: TextStyle(
|
||||
color: cs.outline,
|
||||
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: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isFocused
|
||||
? cs.primary
|
||||
: (hasValue ? cs.outlineVariant : Colors.transparent),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
char,
|
||||
key: ValueKey<String>(char + index.toString()),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GestureDetector(
|
||||
onTap: _resendCode,
|
||||
child: Text(
|
||||
_timerSeconds > 0
|
||||
? 'Отправить повторно через $_timerSeconds сек.'
|
||||
: 'Отправить код по SMS',
|
||||
style: TextStyle(
|
||||
color: cs.tertiary,
|
||||
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
|
||||
? cs.primaryContainer
|
||||
: cs.surfaceContainerHighest,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
color: _codeController.text.length == 5 ? cs.onPrimaryContainer : cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,897 @@
|
||||
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 'package:komet/core/config/countries.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'code_confirmation_screen.dart';
|
||||
import 'select_country_screen.dart';
|
||||
import 'spoff_redacted_screen.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
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<void> _checkTOS() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isTOSRead = prefs.getBool('IsReadeTOS') ?? false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<CountryName>(
|
||||
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: cs.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (context) {
|
||||
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: [
|
||||
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<ScrollUpdateNotification>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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 cs = Theme.of(context).colorScheme;
|
||||
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: cs.surfaceContainerHigh,
|
||||
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: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${_selectedCountry.phoneCode} $formattedPhone',
|
||||
style: GoogleFonts.inter(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Изменить',
|
||||
style: GoogleFonts.inter(
|
||||
color: cs.primary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CodeConfirmationScreen(
|
||||
phoneNumber:
|
||||
'${_selectedCountry.phoneCode} $formattedPhone',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Готово',
|
||||
style: GoogleFonts.inter(
|
||||
color: cs.primary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _validateAndSubmit() {
|
||||
if (!_isTOSRead) {
|
||||
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(
|
||||
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;
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
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: 44),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _showSecurityOptions(context),
|
||||
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(
|
||||
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.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 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: FloatingActionButton(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField({required String label, required Widget content}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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: cs.primary, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
Positioned(
|
||||
top: -10,
|
||||
left: 20,
|
||||
child: Container(
|
||||
color: cs.surface,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: GoogleFonts.inter(
|
||||
color: cs.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhoneInputFormatter extends TextInputFormatter {
|
||||
final CountryName country;
|
||||
_PhoneInputFormatter(this.country);
|
||||
|
||||
@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 > country.phoneDigits) {
|
||||
text = text.substring(0, country.phoneDigits);
|
||||
}
|
||||
|
||||
final buffer = StringBuffer();
|
||||
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();
|
||||
return TextEditingValue(
|
||||
text: formattedText,
|
||||
selection: TextSelection.collapsed(offset: formattedText.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SelectCountryScreen> createState() => _SelectCountryScreenState();
|
||||
}
|
||||
|
||||
class _SelectCountryScreenState extends State<SelectCountryScreen> {
|
||||
bool _isSearching = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<CountryName> _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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SpoffRedactedScreen> createState() => _SpoffRedactedScreenState();
|
||||
}
|
||||
|
||||
class _SpoffRedactedScreenState extends State<SpoffRedactedScreen> {
|
||||
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<void> _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<void> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,985 @@
|
||||
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';
|
||||
import '../profile/settings_tab.dart';
|
||||
|
||||
class ChatListScreen extends StatefulWidget {
|
||||
const ChatListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChatListScreen> createState() => _ChatListScreenState();
|
||||
}
|
||||
|
||||
class _ChatListScreenState extends State<ChatListScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
String _selectedCategory = 'Все чаты';
|
||||
int _currentNavIndex = 0;
|
||||
bool _isFabOpen = false;
|
||||
late AnimationController _fabController;
|
||||
final Set<String> _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() {
|
||||
super.initState();
|
||||
_fabController = AnimationController(
|
||||
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();
|
||||
}
|
||||
|
||||
void _toggleFab() {
|
||||
setState(() {
|
||||
_isFabOpen = !_isFabOpen;
|
||||
if (_isFabOpen) {
|
||||
_fabController.forward();
|
||||
} else {
|
||||
_fabController.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
IndexedStack(
|
||||
index: _currentNavIndex,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
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<int>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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('Архив'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: _isSelectionMode ? -100 : 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 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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 (!_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,
|
||||
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: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStoryItem(String name, String imageUrl, bool hasUpdate) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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: cs.primary, width: 2)
|
||||
: Border.all(color: cs.outlineVariant),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
backgroundImage: NetworkImage(imageUrl),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 ? cs.primaryContainer : cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSelected ? cs.onPrimaryContainer : cs.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChatItem(
|
||||
String id,
|
||||
String name,
|
||||
String message,
|
||||
String time,
|
||||
String imageUrl, {
|
||||
bool isOnline = false,
|
||||
bool isTyping = false,
|
||||
bool isRead = false,
|
||||
int unreadCount = 0,
|
||||
bool isMuted = false,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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,
|
||||
),
|
||||
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(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(int index, IconData icon, String label) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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 ? cs.onPrimary : cs.onSurface,
|
||||
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: TextStyle(
|
||||
color: cs.onPrimary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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<int> _buildPopupMenuItem(
|
||||
int value,
|
||||
String title,
|
||||
IconData icon,
|
||||
) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return PopupMenuItem<int>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class SettingsTab extends StatelessWidget {
|
||||
const SettingsTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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});
|
||||
}
|
||||
@@ -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<CustomNotification> createState() => _CustomNotificationState();
|
||||
}
|
||||
|
||||
class _CustomNotificationState extends State<CustomNotification>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _opacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
reverseDuration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_opacity = Tween<double>(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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+39
-105
@@ -1,3 +1,4 @@
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'backend/api.dart';
|
||||
import 'core/storage/app_database.dart';
|
||||
@@ -14,116 +15,49 @@ void main() async {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
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 DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
final baseScheme = darkDynamic ?? ColorScheme.fromSeed(
|
||||
seedColor: _fallbackSeed,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
|
||||
final darkScheme = _adjustScheme(baseScheme);
|
||||
|
||||
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<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
useMaterial3: true,
|
||||
colorScheme: darkScheme,
|
||||
textTheme: GoogleFonts.interTextTheme(
|
||||
ThemeData.dark().textTheme,
|
||||
),
|
||||
),
|
||||
home: const LoginScreen(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-7
@@ -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:
|
||||
@@ -284,18 +284,26 @@ 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:
|
||||
name: material_symbols_icons
|
||||
sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2906.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -321,6 +329,19 @@ packages:
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
objective_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:
|
||||
name: objective_c
|
||||
@@ -521,7 +542,15 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
|
||||
@@ -44,6 +44,10 @@ dependencies:
|
||||
sqflite: ^2.4.2
|
||||
sqflite_common_ffi: ^2.4.0+2
|
||||
path: ^1.9.1
|
||||
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:
|
||||
@@ -66,6 +70,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:
|
||||
|
||||
Reference in New Issue
Block a user