Что-то похожее на фронтенд

This commit is contained in:
noxzion
2026-03-15 18:34:57 +05:00
parent 06ee2f92e5
commit a5d3c94375
7 changed files with 1419 additions and 105 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

@@ -0,0 +1,228 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../chats/chat_list_screen.dart';
class CodeConfirmationScreen extends StatefulWidget {
final String phoneNumber;
const CodeConfirmationScreen({super.key, required this.phoneNumber});
@override
State<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();
// TODO: Implement actual resend logic here
print('Resending code to ${widget.phoneNumber}');
}
}
void _navigateToChats() {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const ChatListScreen()),
(route) => false,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D12),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white70),
onPressed: () => Navigator.pop(context),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
widget.phoneNumber,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
),
const SizedBox(height: 12),
const Text(
'Мы отправили SMS с кодом подтверждения на ваш номер телефона.',
style: TextStyle(
color: Colors.white54,
fontSize: 15,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
const SizedBox(height: 12),
Stack(
children: [
Opacity(
opacity: 0,
child: SizedBox(
height: 0,
width: 0,
child: TextField(
controller: _codeController,
focusNode: _focusNode,
keyboardType: TextInputType.number,
autofillHints: const [AutofillHints.oneTimeCode],
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(5),
],
onChanged: (value) {
setState(() {});
if (value.length == 5) {
_navigateToChats();
}
},
),
),
),
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: FittedBox(
child: Row(
children: List.generate(5, (index) {
bool isFocused = _codeController.text.length == index && _focusNode.hasFocus;
bool hasValue = _codeController.text.length > index;
String char = hasValue ? _codeController.text[index] : '';
return Container(
width: 44,
height: 54,
margin: EdgeInsets.only(right: index == 4 ? 0 : 10),
decoration: BoxDecoration(
color: const Color(0xFF1E1E2A),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isFocused
? const Color(0xFFBEC2FF)
: (hasValue ? Colors.white24 : Colors.transparent),
width: 1.5,
),
),
alignment: Alignment.center,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: Text(
char,
key: ValueKey<String>(char + index.toString()),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
),
);
}),
),
),
),
],
),
const SizedBox(height: 24),
GestureDetector(
onTap: _resendCode,
child: Text(
_timerSeconds > 0
? 'Отправить повторно через $_timerSeconds сек.'
: 'Отправить код по SMS',
style: const TextStyle(
color: Color(0xFF63A9F5),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
if (_codeController.text.length == 5) {
_navigateToChats();
}
},
backgroundColor: _codeController.text.length == 5
? const Color(0xffc1c4ff)
: const Color(0xFF1E1E2A),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
child: Icon(
Icons.arrow_forward,
color: _codeController.text.length == 5 ? Colors.black : Colors.white24,
),
),
],
),
const SizedBox(height: 24),
],
),
),
),
);
}
}
+491
View File
@@ -0,0 +1,491 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'code_confirmation_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _phoneController = TextEditingController();
String? _errorText;
void _showTOS(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: const Color(0xFF1E1E2A),
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 1.0,
snap: true,
snapSizes: const [0.7, 1.0],
expand: false,
builder: (context, scrollController) => Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Условия использования',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Symbols.close, color: Colors.white70),
),
],
),
const SizedBox(height: 16),
Expanded(
child: ListView(
controller: scrollController,
children: const [
Text(
'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n'
'1. Статус и отношения\n'
'1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n'
'1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n'
'1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n'
'2. Условия использования\n'
'2.1. Используя Приложение «Komet», вы:\n'
'• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n'
'• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n'
'• Принимаете на себя все риски, связанные с использованием Приложения.\n'
'2.2. Строго запрещено:\n'
'• Использовать Приложение «Komet» для распространения запрещённого контента;\n'
'• Осуществлять массовые рассылки (спам);\n'
'• Нарушать законодательство РФ и международное право;\n'
'• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n'
'2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n'
'2.4. Особенности технического взаимодействия:\n'
'• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n'
'• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n'
'• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n'
'3. Технические аспекты\n'
'3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n'
'3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n'
'3.3. Разработчики не несут ответственности за:\n'
'• Изменения в API оригинального сервиса;\n'
'• Блокировку аккаунтов пользователей;\n'
'• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n'
'4. Конфиденциальность\n'
'4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n'
'4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n'
'4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n'
'5. Ответственность и ограничения\n'
'5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n'
'5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n'
'6. Правовые основания\n'
'6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n'
'• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n'
'• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n'
'• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n'
'• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n'
'6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n'
'• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n'
'• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n'
'• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n'
'6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n'
'7. Заключительные положения\n'
'7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n'
'• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n'
'• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n'
'• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n'
'7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n'
'8. Функции безопасности и конфиденциальности\n'
'8.1. Приложение «Komet» включает инструменты защиты приватности:\n'
'• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов OpenSourceIntelligence (OSINT);\n'
'• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n'
'• Ограничение телеметрии — для минимизации передачи диагностических данных.\n'
'8.2. Данные функции:\n'
'• Направлены исключительно на защиту конфиденциальности пользователей;\n'
'• Не используются для обхода систем безопасности оригинального сервиса;\n'
'• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n'
'8.3. Разработчики не несут ответственности за:\n'
'• Блокировки, связанные с использованием инструментов конфиденциальности;\n'
'• Изменения в работе сервиса при активации данных функций.\n'
'8.4. Функции экспорта и импорта сессии\n'
'8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n'
'• Обеспечения переносимости данных между устройствами пользователя\n'
'• Резервного копирования учетных данных\n'
'• Восстановления доступа при утере устройства\n'
'8.4.2. Особенности реализации:\n'
'• Экспорт сессии осуществляется без привязки к номеру телефона\n'
'• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n'
'• Ключ шифрования известен только пользователю и не сохраняется в приложении\n'
'8.4.3. Техническая реализация экспорта сессии:\n'
'• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n'
'• Используется подмена параметров сессии для сохранения контекста аутентификации\n'
'• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n'
'• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n'
'• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n'
'• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n'
'8.4.4. Правовые основания:\n'
'• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n'
'• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n'
'• Принцип минимизации данных — сбор только необходимой для работы информации\n'
'• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n'
'• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n'
'• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n'
'8.4.5. Ограничения ответственности:\n'
'• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n'
'• Разработчики не имеют доступа к зашифрованным данным сессии\n'
'• Восстановление утерянных паролей невозможно в целях безопасности\n'
'• Ключи шифрования не хранятся в приложении и известны только пользователю',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.5,
),
),
],
),
),
],
),
),
),
);
}
void _validateAndSubmit() {
setState(() {
final phone = _phoneController.text.replaceAll(RegExp(r'\D'), '');
if (phone.isEmpty) {
_errorText = 'Пожалуйста, введите номер телефона';
} else if (phone.length < 10) {
_errorText = 'Номер телефона слишком короткий';
} else {
_errorText = null;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CodeConfirmationScreen(
phoneNumber: '+7 ${_phoneController.text}',
),
),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D12),
body: GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragEnd: (details) {
if (details.primaryVelocity != null && details.primaryVelocity! < -500) {
_showTOS(context);
}
},
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {},
icon: const Icon(
Symbols.admin_panel_settings,
color: Colors.white70,
weight: 400,
),
),
IconButton(
onPressed: () {},
icon: const Icon(
Symbols.language,
color: Colors.white70,
weight: 400,
),
),
],
),
const SizedBox(height: 24),
Center(
child: Column(
children: [
Image.asset(
'assets/komet.png',
height: 80,
color: Colors.white,
),
const SizedBox(height: 16),
const Text(
'Войдите в Komet',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
const Text(
'Проверьте код страны и введите свой\nномер телефона.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
],
),
),
const SizedBox(height: 24),
_buildInputField(
label: 'Страна',
content: Row(
children: [
const Text(
'Россия',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400),
),
const Spacer(),
const Icon(Icons.keyboard_arrow_down, color: Colors.white70),
],
),
),
const SizedBox(height: 12),
_buildInputField(
label: 'Номер телефона',
isError: _errorText != null,
errorText: _errorText,
content: Row(
children: [
const Text(
'+7',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400),
),
const SizedBox(width: 12),
Container(
width: 1,
height: 24,
color: Colors.white24,
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
_PhoneInputFormatter(),
],
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w400),
decoration: const InputDecoration(
hintText: '(000) 000-00-00',
hintStyle: TextStyle(color: Colors.white38, fontSize: 15, fontWeight: FontWeight.w400),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
onChanged: (value) {
if (_errorText != null) {
setState(() {
_errorText = null;
});
}
},
),
),
],
),
),
const SizedBox(height: 24),
TextButton(
onPressed: () {},
child: const Text(
'Другие способы входа',
style: TextStyle(
color: Color(0xFFAFAFFF),
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
const Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: RichText(
text: TextSpan(
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
height: 1.4,
fontFamily: 'Outfit',
),
children: [
const TextSpan(text: 'Продолжая, вы соглашаетесь с\n'),
TextSpan(
text: 'пользовательскими соглашениями',
style: const TextStyle(
color: Color(0xFFAFAFFF),
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()
..onTap = () => _showTOS(context),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: FloatingActionButton(
onPressed: _validateAndSubmit,
backgroundColor: const Color(0xffc1c4ff),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
child: const Icon(Icons.arrow_forward, color: Colors.black),
),
),
],
),
const SizedBox(height: 16),
],
),
),
),
),
);
},
),
),
),
);
}
Widget _buildInputField({
required String label,
required Widget content,
bool isError = false,
String? errorText,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: double.infinity,
height: 54,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(
color: isError ? Colors.redAccent : const Color(0xFFBEC2FF),
width: 1.5,
),
borderRadius: BorderRadius.circular(50),
),
child: content,
),
Positioned(
top: -10,
left: 20,
child: Container(
color: const Color(0xFF0D0D12),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
label,
style: TextStyle(
color: isError ? Colors.redAccent : const Color(0xFFBEC2FF),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
),
],
),
if (isError && errorText != null) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
errorText,
style: const TextStyle(
color: Colors.redAccent,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
],
],
);
}
}
class _PhoneInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
var text = newValue.text.replaceAll(RegExp(r'\D'), '');
if (newValue.text.length < oldValue.text.length) {
final oldDigits = oldValue.text.replaceAll(RegExp(r'\D'), '');
if (text.length == oldDigits.length && text.isNotEmpty) {
text = text.substring(0, text.length - 1);
}
}
if (text.length > 10) text = text.substring(0, 10);
final buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
if (i == 0) buffer.write('(');
buffer.write(text[i]);
if (i == 2) buffer.write(') ');
if (i == 5) buffer.write('-');
}
final formattedText = buffer.toString();
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: formattedText.length),
);
}
}
@@ -0,0 +1,478 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'dart:ui' as ui;
class ChatListScreen extends StatefulWidget {
const ChatListScreen({super.key});
@override
State<ChatListScreen> createState() => _ChatListScreenState();
}
class _ChatListScreenState extends State<ChatListScreen> {
String _selectedCategory = 'Все чаты';
int _currentNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D12),
body: SafeArea(
bottom: false,
child: Stack(
children: [
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Подключение...',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
),
IconButton(
onPressed: () {},
icon: const Icon(
Symbols.more_vert,
color: Colors.white54,
weight: 400,
),
),
],
),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 96,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
_buildStoryItem('Даша', 'https://i.pravatar.cc/150?u=dasha', true),
_buildStoryItem('Мастика', 'https://i.pravatar.cc/150?u=mastika', false),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
sliver: SliverToBoxAdapter(
child: Container(
height: 44,
decoration: BoxDecoration(
color: const Color(0xFF1E1E2A),
borderRadius: BorderRadius.circular(50),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: const Row(
children: [
Icon(Symbols.search, color: Colors.white38, size: 20, weight: 400),
SizedBox(width: 10),
Expanded(
child: TextField(
style: TextStyle(color: Colors.white, fontSize: 15),
decoration: InputDecoration(
hintText: 'Поиск',
hintStyle: TextStyle(color: Colors.white38, fontSize: 15),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 48,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
ui.PointerDeviceKind.touch,
ui.PointerDeviceKind.mouse,
ui.PointerDeviceKind.trackpad,
},
),
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
physics: const BouncingScrollPhysics(),
children: [
_buildCategoryChip('Все чаты'),
const SizedBox(width: 8),
_buildCategoryChip('Контакты'),
const SizedBox(width: 8),
_buildCategoryChip('Пидоры'),
const SizedBox(width: 8),
_buildCategoryChip('Каналы'),
const SizedBox(width: 8),
_buildCategoryChip('Группы'),
const SizedBox(width: 8),
_buildCategoryChip('Боты'),
const SizedBox(width: 8),
_buildCategoryChip('Избранное'),
const SizedBox(width: 8),
_buildCategoryChip('Архив'),
],
),
),
),
),
SliverList(
delegate: SliverChildListDelegate([
_buildChatItem(
'Станислав',
'Хорошо',
'10:07',
'https://i.pravatar.cc/150?u=stas',
isOnline: true,
isRead: true,
),
_buildChatItem(
'Илья',
'печатает...',
'10:07',
'https://i.pravatar.cc/150?u=ilya',
isOnline: true,
isTyping: true,
unreadCount: 1,
),
_buildChatItem(
'Вероника',
'Спасибо',
'09:56',
'https://i.pravatar.cc/150?u=veronika',
isRead: true,
),
_buildChatItem(
'Komet Client',
'Кстати. Смотрите, какую шту...',
'09:56',
'https://i.pravatar.cc/150?u=komet',
unreadCount: 5,
isMuted: true,
),
_buildChatItem(
'4-й подъезд',
'Людмила: Сколько?',
'09:34',
'https://i.pravatar.cc/150?u=podezd',
unreadCount: 78,
isMuted: true,
),
const SizedBox(height: 100),
]),
),
],
),
Positioned(
left: 8,
right: 8,
bottom: 24,
child: Container(
height: 68,
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: const Color(0xFF1E1E28),
borderRadius: BorderRadius.circular(34),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: LayoutBuilder(
builder: (context, constraints) {
double totalWidth = constraints.maxWidth;
double totalWeight = 5.2;
double unitWidth = totalWidth / totalWeight;
double activeWidth = unitWidth * 2.2;
double inactiveWidth = unitWidth * 1.0;
double leftOffset = 0;
for (int i = 0; i < _currentNavIndex; i++) {
leftOffset += inactiveWidth;
}
return Stack(
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
left: leftOffset + 4,
top: 8,
bottom: 8,
width: activeWidth - 8,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFBEC2FF),
borderRadius: BorderRadius.circular(26),
),
),
),
Row(
children: List.generate(4, (index) {
IconData icon;
String label;
switch (index) {
case 0: icon = Symbols.chat_bubble; label = 'Чаты'; break;
case 1: icon = Symbols.call; label = 'Звонки'; break;
case 2: icon = Symbols.person_pin; label = 'Контакты'; break;
default: icon = Symbols.settings; label = 'Настройки';
}
bool isSelected = _currentNavIndex == index;
return AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
width: isSelected ? (activeWidth - 0.5) : (inactiveWidth - 0.5),
child: ClipRRect(
borderRadius: BorderRadius.circular(26),
child: _buildNavItem(index, icon, label),
),
);
}),
),
],
);
},
),
),
),
Positioned(
right: 20,
bottom: 110,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: const Color(0xFFC1C4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Symbols.add, color: Colors.black, size: 28, weight: 400),
),
),
],
),
),
);
}
Widget _buildStoryItem(String name, String imageUrl, bool hasUpdate) {
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(2.5),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: hasUpdate
? Border.all(color: const Color(0xFFC1C4FF), width: 2)
: Border.all(color: Colors.white10),
),
child: CircleAvatar(
radius: 26,
backgroundImage: NetworkImage(imageUrl),
),
),
const SizedBox(height: 6),
Text(
name,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildCategoryChip(String title) {
bool isSelected = _selectedCategory == title;
return GestureDetector(
onTap: () => setState(() => _selectedCategory = title),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFC1C4FF) : const Color(0xFF1E1E2A),
borderRadius: BorderRadius.circular(15),
),
child: Text(
title,
style: TextStyle(
color: isSelected ? Colors.black : Colors.white54,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
);
}
Widget _buildChatItem(
String name,
String message,
String time,
String imageUrl, {
bool isOnline = false,
bool isTyping = false,
bool isRead = false,
int unreadCount = 0,
bool isMuted = false,
}) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
leading: Stack(
children: [
CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(imageUrl),
),
if (isOnline)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: const Color(0xFFC1C4FF),
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFF0D0D12), width: 2),
),
),
),
],
),
title: Row(
children: [
Expanded(
child: Text(
name,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
if (isMuted)
const Icon(Symbols.notifications_off, color: Colors.white24, size: 14, weight: 400),
const SizedBox(width: 8),
Text(
time,
style: const TextStyle(
color: Colors.white38,
fontSize: 12,
),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
Expanded(
child: Text(
message,
style: TextStyle(
color: isTyping ? const Color(0xFFC1C4FF) : Colors.white54,
fontSize: 14,
fontWeight: isTyping ? FontWeight.w500 : FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isMuted ? Colors.white10 : const Color(0xFF1E1E2A),
borderRadius: BorderRadius.circular(10),
),
child: Text(
unreadCount.toString(),
style: TextStyle(
color: isMuted ? Colors.white38 : Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
)
else if (isRead)
const Icon(Symbols.done_all, color: Color(0xFFC1C4FF), size: 16, weight: 400),
],
),
),
);
}
Widget _buildNavItem(int index, IconData icon, String label) {
bool isSelected = _currentNavIndex == index;
return GestureDetector(
onTap: () => setState(() => _currentNavIndex = index),
behavior: HitTestBehavior.opaque,
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: isSelected ? Colors.black : Colors.white,
size: 20,
weight: 400,
fill: isSelected ? 1.0 : 0.0,
),
AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
width: isSelected ? null : 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isSelected ? 1.0 : 0.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(
color: Colors.black,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
),
);
}
}
+9 -104
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:komet/frontend/screens/auth/login_screen.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@@ -7,116 +9,19 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Komet',
debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
// This is the theme of your application. useMaterial3: true,
// brightness: Brightness.dark,
// TRY THIS: Try running your application with "flutter run". You'll see textTheme: GoogleFonts.outfitTextTheme(
// the application has a purple toolbar. Then, without quitting the app, ThemeData.dark().textTheme,
// 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( home: const LoginScreen(),
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
); );
} }
} }
+209 -1
View File
@@ -33,6 +33,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +49,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -65,6 +81,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -83,6 +115,46 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
url: "https://pub.dev"
source: hosted
version: "1.0.2"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -123,6 +195,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.2" version: "2.6.2"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -139,6 +219,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082
url: "https://pub.dev"
source: hosted
version: "4.2906.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -155,6 +243,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f"
url: "https://pub.dev"
source: hosted
version: "0.17.5"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -163,6 +267,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -216,6 +392,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.7"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -232,6 +416,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.10.4 <4.0.0" dart: ">=3.10.4 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.38.4"
+4
View File
@@ -37,6 +37,8 @@ dependencies:
dart_lz4: ^1.0.0 dart_lz4: ^1.0.0
msgpack_dart: ^1.0.1 msgpack_dart: ^1.0.1
logger: ^2.6.2 logger: ^2.6.2
google_fonts: ^6.2.1
material_symbols_icons: ^4.2906.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -59,6 +61,8 @@ flutter:
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
uses-material-design: true uses-material-design: true
assets:
- assets/komet.png
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: # assets: