From f93a89b9f3f09aef8fb6c6209f0f301aba59b88e Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Thu, 26 Mar 2026 12:06:42 +0700 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D0=BB=D1=83=D1=88=D0=B5=D0=BA=20=D0=BF=D0=BE=D1=82=D1=8B=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/config/spoof_data.dart | 31 + lib/frontend/screens/auth/login_screen.dart | 145 ++-- .../screens/auth/spoff_redacted_screen.dart | 217 ++++++ lib/frontend/screens/calls/calls_tab.dart | 21 + .../screens/chats/chat_list_screen.dart | 706 ++++++++++++------ .../screens/contacts/contacts_tab.dart | 21 + .../screens/profile/settings_tab.dart | 21 + lib/frontend/widgets/custom_notification.dart | 81 ++ 8 files changed, 927 insertions(+), 316 deletions(-) create mode 100644 lib/core/config/spoof_data.dart create mode 100644 lib/frontend/screens/auth/spoff_redacted_screen.dart create mode 100644 lib/frontend/screens/calls/calls_tab.dart create mode 100644 lib/frontend/screens/contacts/contacts_tab.dart create mode 100644 lib/frontend/screens/profile/settings_tab.dart create mode 100644 lib/frontend/widgets/custom_notification.dart diff --git a/lib/core/config/spoof_data.dart b/lib/core/config/spoof_data.dart new file mode 100644 index 0000000..f60c3ff --- /dev/null +++ b/lib/core/config/spoof_data.dart @@ -0,0 +1,31 @@ +class SpoofData { + static const List deviceNames = [ + 'Samsung Galaxy S23', + 'Xiaomi 13 Pro', + 'Google Pixel 7', + 'OnePlus 11', + ]; + + static const List osVersions = ['12', '13', '14']; + + static const List resolutions = [ + '1080x2400', + '1440x3200', + '720x1600', + ]; + + static const List deviceIds = [ + 'a1b2c3d4e5f6', + 'f8e7d6c5b4a3', + '9876543210ab', + '1234567890cd', + ]; + + static const List architectures = ['arm64-v8a', 'armeabi-v7a']; + + static const String deviceType = 'android'; + static const String timezone = 'Europe/Moscow'; + static const String locale = 'ru_RU'; + static const String appVersion = '26.10.1'; + static const String buildNumber = '6728'; +} diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index 44a8de4..7a705d7 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -7,85 +7,8 @@ import 'package:komet/core/config/countries.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'code_confirmation_screen.dart'; import 'select_country_screen.dart'; - -void _showCustomNotification(BuildContext context, String message) { - final overlay = Overlay.of(context); - final entry = OverlayEntry( - builder: (context) => _CustomNotification(message: message), - ); - overlay.insert(entry); - Future.delayed(const Duration(milliseconds: 1900), () { - entry.remove(); - }); -} - -class _CustomNotification extends StatefulWidget { - final String message; - const _CustomNotification({required this.message}); - - @override - State<_CustomNotification> createState() => _CustomNotificationState(); -} - -class _CustomNotificationState extends State<_CustomNotification> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _opacity; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - ); - _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller); - _controller.forward(); - Future.delayed(const Duration(milliseconds: 1600), () { - if (mounted) _controller.reverse(); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return Positioned( - bottom: 60, - left: 0, - right: 0, - child: Material( - color: Colors.transparent, - child: Center( - child: FadeTransition( - opacity: _opacity, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), - ), - child: Text( - widget.message, - style: GoogleFonts.inter( - color: cs.onSurface, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ), - ); - } -} +import 'spoff_redacted_screen.dart'; +import '../../widgets/custom_notification.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -482,12 +405,72 @@ class _LoginScreenState extends State { void _validateAndSubmit() { if (!_isTOSRead) { - _showCustomNotification(context, 'Соглащение прочитай щегол'); + showCustomNotification(context, 'Соглащение прочитай щегол'); return; } _showPhoneConfirmationDialog(_phoneController.text); } + void _showSecurityOptions(BuildContext context) { + final cs = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: cs.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Symbols.security, color: cs.onSurface), + title: Text( + 'Подделка спуфа', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SpoffRedactedScreen(), + ), + ); + }, + ), + ListTile( + leading: Icon(Symbols.vpn_lock, color: cs.onSurface), + title: Text( + 'Прокси', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + } + void _showOtherLoginMethods(BuildContext context) { final cs = Theme.of(context).colorScheme; showModalBottomSheet( @@ -588,7 +571,7 @@ class _LoginScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( - onPressed: () {}, + onPressed: () => _showSecurityOptions(context), icon: Icon( Symbols.admin_panel_settings, color: cs.onSurfaceVariant, diff --git a/lib/frontend/screens/auth/spoff_redacted_screen.dart b/lib/frontend/screens/auth/spoff_redacted_screen.dart new file mode 100644 index 0000000..a553a59 --- /dev/null +++ b/lib/frontend/screens/auth/spoff_redacted_screen.dart @@ -0,0 +1,217 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:komet/core/config/spoof_data.dart'; +import 'package:komet/frontend/widgets/custom_notification.dart'; + +class SpoffRedactedScreen extends StatefulWidget { + const SpoffRedactedScreen({super.key}); + + @override + State createState() => _SpoffRedactedScreenState(); +} + +class _SpoffRedactedScreenState extends State { + final TextEditingController _deviceNameController = TextEditingController(); + final TextEditingController _osVersionController = TextEditingController(); + final TextEditingController _resolutionController = TextEditingController(); + final TextEditingController _deviceIdController = TextEditingController(); + final TextEditingController _architectureController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + + final random = Random(); + + setState(() { + _deviceNameController.text = + prefs.getString('spoof_device_name') ?? + SpoofData.deviceNames[random.nextInt(SpoofData.deviceNames.length)]; + _osVersionController.text = + prefs.getString('spoof_os_version') ?? + SpoofData.osVersions[random.nextInt(SpoofData.osVersions.length)]; + _resolutionController.text = + prefs.getString('spoof_resolution') ?? + SpoofData.resolutions[random.nextInt(SpoofData.resolutions.length)]; + _deviceIdController.text = + prefs.getString('spoof_device_id') ?? + SpoofData.deviceIds[random.nextInt(SpoofData.deviceIds.length)]; + _architectureController.text = + prefs.getString('spoof_architecture') ?? + SpoofData.architectures[random.nextInt( + SpoofData.architectures.length, + )]; + }); + } + + Future _saveSettings() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('spoof_device_name', _deviceNameController.text); + await prefs.setString('spoof_os_version', _osVersionController.text); + await prefs.setString('spoof_resolution', _resolutionController.text); + await prefs.setString('spoof_device_id', _deviceIdController.text); + await prefs.setString('spoof_architecture', _architectureController.text); + + if (mounted) { + showCustomNotification(context, 'Настройки сохранены'); + } + } + + @override + void dispose() { + _deviceNameController.dispose(); + _osVersionController.dispose(); + _resolutionController.dispose(); + _deviceIdController.dispose(); + _architectureController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + iconTheme: IconThemeData(color: cs.onSurface), + title: Text( + 'Подделка спуфа', + style: GoogleFonts.inter( + color: cs.onSurface, + fontWeight: FontWeight.w500, + ), + ), + backgroundColor: cs.surface, + actions: [ + IconButton( + icon: Icon(Icons.check, color: cs.primary), + onPressed: _saveSettings, + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildTextField( + controller: TextEditingController(text: SpoofData.deviceType), + label: 'Тип устройства', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _deviceNameController, + label: 'Имя устройства', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _osVersionController, + label: 'Версия ОС', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _resolutionController, + label: 'Разрешение экрана', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.timezone), + label: 'Часовой пояс', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.locale), + label: 'Локаль', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _deviceIdController, + label: 'ID устройства', + cs: cs, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.appVersion), + label: 'Версия приложения', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: TextEditingController(text: SpoofData.buildNumber), + label: 'Build Number', + cs: cs, + readOnly: true, + ), + const SizedBox(height: 16), + _buildTextField( + controller: _architectureController, + label: 'Архитектура', + cs: cs, + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required ColorScheme cs, + bool readOnly = false, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.inter( + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + readOnly: readOnly, + style: GoogleFonts.inter( + color: readOnly ? cs.onSurfaceVariant : cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + filled: true, + fillColor: readOnly + ? cs.surfaceContainerHighest + : cs.surfaceContainerHigh, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + ), + ], + ); + } +} diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart new file mode 100644 index 0000000..21bf27b --- /dev/null +++ b/lib/frontend/screens/calls/calls_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class CallsTab extends StatelessWidget { + const CallsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Звонки (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 4260cd8..06326fc 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'dart:math'; import 'dart:ui' as ui; +import '../calls/calls_tab.dart'; +import '../contacts/contacts_tab.dart'; +import '../profile/settings_tab.dart'; + class ChatListScreen extends StatefulWidget { const ChatListScreen({super.key}); @@ -9,9 +14,38 @@ class ChatListScreen extends StatefulWidget { State createState() => _ChatListScreenState(); } -class _ChatListScreenState extends State { +class _ChatListScreenState extends State + with SingleTickerProviderStateMixin { String _selectedCategory = 'Все чаты'; int _currentNavIndex = 0; + bool _isFabOpen = false; + late AnimationController _fabController; + + @override + void initState() { + super.initState(); + _fabController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 350), + ); + } + + @override + void dispose() { + _fabController.dispose(); + super.dispose(); + } + + void _toggleFab() { + setState(() { + _isFabOpen = !_isFabOpen; + if (_isFabOpen) { + _fabController.forward(); + } else { + _fabController.reverse(); + } + }); + } @override Widget build(BuildContext context) { @@ -22,250 +56,375 @@ class _ChatListScreenState extends State { bottom: false, child: Stack( children: [ - CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), - sliver: SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Подключение...', - style: TextStyle( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), - ), - IconButton( - onPressed: () {}, - icon: Icon( - Symbols.more_vert, - color: cs.outline, - weight: 400, - ), - ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 96, - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20), - children: [ - _buildStoryItem('Даша', 'https://i.pravatar.cc/150?u=dasha', true), - _buildStoryItem('Мастика', 'https://i.pravatar.cc/150?u=mastika', false), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - sliver: SliverToBoxAdapter( - child: Container( - height: 44, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(50), - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Icon(Symbols.search, color: cs.outline, size: 20, weight: 400), - const SizedBox(width: 10), - Expanded( - child: TextField( - style: TextStyle(color: cs.onSurface, fontSize: 15), - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle(color: cs.outline, fontSize: 15), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, + IndexedStack( + index: _currentNavIndex, + children: [ + CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Подключение...', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', ), ), + PopupMenuButton( + icon: Icon( + Symbols.more_vert, + color: cs.outline, + weight: 400, + ), + offset: const Offset(0, 48), + elevation: 4, + color: cs.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + itemBuilder: (context) => [ + _buildPopupMenuItem( + 1, + 'Кнопка 1', + Symbols.settings, + ), + _buildPopupMenuItem( + 2, + 'Кнопка 2', + Symbols.notifications, + ), + _buildPopupMenuItem( + 3, + 'Кнопка 3', + Symbols.shield, + ), + _buildPopupMenuItem( + 4, + 'Кнопка 4', + Symbols.info, + ), + ], + onSelected: (value) { + // Action handler + }, + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 96, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + _buildStoryItem( + 'Даша', + 'https://i.pravatar.cc/150?u=dasha', + true, + ), + _buildStoryItem( + 'Мастика', + 'https://i.pravatar.cc/150?u=mastika', + false, + ), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + sliver: SliverToBoxAdapter( + child: Container( + height: 44, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), ), - ], + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon( + Symbols.search, + color: cs.outline, + size: 20, + weight: 400, + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ), ), ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 48, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - ui.PointerDeviceKind.touch, - ui.PointerDeviceKind.mouse, - ui.PointerDeviceKind.trackpad, - }, - ), - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - physics: const BouncingScrollPhysics(), - children: [ - _buildFolderChip('Все чаты'), - const SizedBox(width: 8), - _buildFolderChip('Контакты'), - const SizedBox(width: 8), - _buildFolderChip('Пидоры'), - const SizedBox(width: 8), - _buildFolderChip('Каналы'), - const SizedBox(width: 8), - _buildFolderChip('Группы'), - const SizedBox(width: 8), - _buildFolderChip('Боты'), - const SizedBox(width: 8), - _buildFolderChip('Избранное'), - const SizedBox(width: 8), - _buildFolderChip('Архив'), - ], + SliverToBoxAdapter( + child: SizedBox( + height: 48, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + ui.PointerDeviceKind.trackpad, + }, + ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + physics: const BouncingScrollPhysics(), + children: [ + _buildFolderChip('Все чаты'), + const SizedBox(width: 8), + _buildFolderChip('Контакты'), + const SizedBox(width: 8), + _buildFolderChip('Пидоры'), + const SizedBox(width: 8), + _buildFolderChip('Каналы'), + const SizedBox(width: 8), + _buildFolderChip('Группы'), + const SizedBox(width: 8), + _buildFolderChip('Боты'), + const SizedBox(width: 8), + _buildFolderChip('Избранное'), + const SizedBox(width: 8), + _buildFolderChip('Архив'), + ], + ), + ), ), ), - ), - ), - SliverList( - delegate: SliverChildListDelegate([ - _buildChatItem( - 'Станислав', - 'Хорошо', - '10:07', - 'https://i.pravatar.cc/150?u=stas', - isOnline: true, - isRead: true, + SliverList( + delegate: SliverChildListDelegate([ + _buildChatItem( + 'Станислав', + 'Хорошо', + '10:07', + 'https://i.pravatar.cc/150?u=stas', + isOnline: true, + isRead: true, + ), + _buildChatItem( + 'Илья', + 'печатает...', + '10:07', + 'https://i.pravatar.cc/150?u=ilya', + isOnline: true, + isTyping: true, + unreadCount: 1, + ), + _buildChatItem( + 'Вероника', + 'Спасибо', + '09:56', + 'https://i.pravatar.cc/150?u=veronika', + isRead: true, + ), + _buildChatItem( + 'Komet Client', + 'Кстати. Смотрите, какую шту...', + '09:56', + 'https://i.pravatar.cc/150?u=komet', + unreadCount: 5, + isMuted: true, + ), + _buildChatItem( + '4-й подъезд', + 'Людмила: Сколько?', + '09:34', + 'https://i.pravatar.cc/150?u=podezd', + unreadCount: 78, + isMuted: true, + ), + const SizedBox(height: 100), + ]), ), - _buildChatItem( - 'Илья', - 'печатает...', - '10:07', - 'https://i.pravatar.cc/150?u=ilya', - isOnline: true, - isTyping: true, - unreadCount: 1, - ), - _buildChatItem( - 'Вероника', - 'Спасибо', - '09:56', - 'https://i.pravatar.cc/150?u=veronika', - isRead: true, - ), - _buildChatItem( - 'Komet Client', - 'Кстати. Смотрите, какую шту...', - '09:56', - 'https://i.pravatar.cc/150?u=komet', - unreadCount: 5, - isMuted: true, - ), - _buildChatItem( - '4-й подъезд', - 'Людмила: Сколько?', - '09:34', - 'https://i.pravatar.cc/150?u=podezd', - unreadCount: 78, - isMuted: true, - ), - const SizedBox(height: 100), - ]), + ], ), + const CallsTab(), + const ContactsTab(), + const SettingsTab(), ], ), Positioned( left: 8, right: 8, - bottom: 24, - child: Container( - height: 68, - padding: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(34), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: LayoutBuilder( - builder: (context, constraints) { - double totalWidth = constraints.maxWidth; + bottom: 24.0, + child: RepaintBoundary( + child: Container( + height: 68, + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(34), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + double totalWidth = constraints.maxWidth; - double totalWeight = 5.2; - double unitWidth = totalWidth / totalWeight; - double activeWidth = unitWidth * 2.2; - double inactiveWidth = unitWidth * 1.0; + double totalWeight = 5.2; + double unitWidth = totalWidth / totalWeight; + double activeWidth = unitWidth * 2.2; + double inactiveWidth = unitWidth * 1.0; - double leftOffset = 0; - for (int i = 0; i < _currentNavIndex; i++) { - leftOffset += inactiveWidth; - } + double leftOffset = 0; + for (int i = 0; i < _currentNavIndex; i++) { + leftOffset += inactiveWidth; + } - return Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 350), - curve: Curves.easeOutCubic, - left: leftOffset + 4, - top: 8, - bottom: 8, - width: activeWidth - 8, - child: Container( - decoration: BoxDecoration( - color: cs.primary, - borderRadius: BorderRadius.circular(26), + return Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + left: leftOffset + 4, + top: 8, + bottom: 8, + width: activeWidth - 8, + child: Container( + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(26), + ), ), ), - ), - Row( - children: List.generate(4, (index) { - IconData icon; - String label; - switch (index) { - case 0: icon = Symbols.chat_bubble; label = 'Чаты'; break; - case 1: icon = Symbols.call; label = 'Звонки'; break; - case 2: icon = Symbols.person_pin; label = 'Контакты'; break; - default: icon = Symbols.settings; label = 'Настройки'; - } + Row( + children: List.generate(4, (index) { + IconData icon; + String label; + switch (index) { + case 0: + icon = Symbols.chat_bubble; + label = 'Чаты'; + break; + case 1: + icon = Symbols.call; + label = 'Звонки'; + break; + case 2: + icon = Symbols.person_pin; + label = 'Контакты'; + break; + default: + icon = Symbols.settings; + label = 'Настройки'; + } - bool isSelected = _currentNavIndex == index; - return AnimatedContainer( - duration: const Duration(milliseconds: 350), - curve: Curves.easeOutCubic, - width: isSelected ? (activeWidth - 0.5) : (inactiveWidth - 0.5), - child: ClipRRect( - borderRadius: BorderRadius.circular(26), - child: _buildNavItem(index, icon, label), - ), - ); - }), - ), - ], - ); - }, + bool isSelected = _currentNavIndex == index; + return AnimatedContainer( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + width: isSelected + ? (activeWidth - 0.5) + : (inactiveWidth - 0.5), + child: ClipRRect( + borderRadius: BorderRadius.circular(26), + child: _buildNavItem(index, icon, label), + ), + ); + }), + ), + ], + ); + }, + ), ), ), ), - Positioned( - right: 20, - bottom: 110, - child: FloatingActionButton( - onPressed: () {}, - backgroundColor: cs.primaryContainer, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Icon(Symbols.add, color: cs.onPrimaryContainer, size: 28, weight: 400), - ), + AnimatedBuilder( + animation: _fabController, + builder: (context, child) { + final double val = Curves.easeOutCubic.transform( + _fabController.value, + ); + return Stack( + clipBehavior: Clip.none, + children: [ + if (_fabController.value > 0) + Positioned.fill( + child: GestureDetector( + onTap: _toggleFab, + behavior: HitTestBehavior.opaque, + child: Container( + color: Colors.black.withValues(alpha: val * 0.2), + ), + ), + ), + if (_fabController.value > 0) + Positioned( + right: 20, + bottom: 110 + 74, + child: RepaintBoundary( + child: Transform.scale( + scale: val, + alignment: Alignment.bottomRight, + child: Opacity( + opacity: val > 0.5 ? (val - 0.5) * 2 : 0, + child: _buildFabMenu(), + ), + ), + ), + ), + Positioned( + right: 20, + bottom: 110, + child: FloatingActionButton( + onPressed: _toggleFab, + backgroundColor: cs.primaryContainer, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Transform.rotate( + angle: val * (pi / 4), + child: Icon( + Symbols.add, + color: cs.onPrimaryContainer, + size: 28, + weight: 400, + ), + ), + ), + ), + ], + ); + }, ), ], ), @@ -345,10 +504,7 @@ class _ChatListScreenState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), leading: Stack( children: [ - CircleAvatar( - radius: 24, - backgroundImage: NetworkImage(imageUrl), - ), + CircleAvatar(radius: 24, backgroundImage: NetworkImage(imageUrl)), if (isOnline) Positioned( right: 0, @@ -378,15 +534,14 @@ class _ChatListScreenState extends State { ), ), if (isMuted) - Icon(Symbols.notifications_off, color: cs.outlineVariant, size: 14, weight: 400), - const SizedBox(width: 8), - Text( - time, - style: TextStyle( - color: cs.outline, - fontSize: 12, + Icon( + Symbols.notifications_off, + color: cs.outlineVariant, + size: 14, + weight: 400, ), - ), + const SizedBox(width: 8), + Text(time, style: TextStyle(color: cs.outline, fontSize: 12)), ], ), subtitle: Padding( @@ -409,7 +564,9 @@ class _ChatListScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isMuted ? cs.surfaceContainerHighest : cs.surfaceContainerHigh, + color: isMuted + ? cs.surfaceContainerHighest + : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(10), ), child: Text( @@ -478,4 +635,83 @@ class _ChatListScreenState extends State { ), ); } + + Widget _buildFabMenu() { + final cs = Theme.of(context).colorScheme; + return Container( + width: 220, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFabMenuItem(Symbols.search, 'Найти по номеру'), + _buildFabMenuItem(Symbols.group_add, 'Добавить группу'), + _buildFabMenuItem(Symbols.campaign, 'Создать канал'), + ], + ), + ); + } + + Widget _buildFabMenuItem(IconData icon, String title) { + final cs = Theme.of(context).colorScheme; + return InkWell( + onTap: () { + // Action logic here + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 22), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + PopupMenuItem _buildPopupMenuItem( + int value, + String title, + IconData icon, + ) { + final cs = Theme.of(context).colorScheme; + return PopupMenuItem( + value: value, + child: Row( + children: [ + Icon(icon, color: cs.onSurface, size: 20), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } } diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart new file mode 100644 index 0000000..c47b562 --- /dev/null +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ContactsTab extends StatelessWidget { + const ContactsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Контакты (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart new file mode 100644 index 0000000..2a60054 --- /dev/null +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class SettingsTab extends StatelessWidget { + const SettingsTab({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( + child: Text( + 'Настройки (Заглушка)', + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/frontend/widgets/custom_notification.dart b/lib/frontend/widgets/custom_notification.dart new file mode 100644 index 0000000..85c6e91 --- /dev/null +++ b/lib/frontend/widgets/custom_notification.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +void showCustomNotification(BuildContext context, String message) { + final overlay = Overlay.of(context); + final entry = OverlayEntry( + builder: (context) => CustomNotification(message: message), + ); + overlay.insert(entry); + Future.delayed(const Duration(milliseconds: 1900), () { + entry.remove(); + }); +} + +class CustomNotification extends StatefulWidget { + final String message; + const CustomNotification({required this.message, super.key}); + + @override + State createState() => _CustomNotificationState(); +} + +class _CustomNotificationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + ); + _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller); + _controller.forward(); + Future.delayed(const Duration(milliseconds: 1600), () { + if (mounted) _controller.reverse(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Positioned( + bottom: 60, + left: 0, + right: 0, + child: Material( + color: Colors.transparent, + child: Center( + child: FadeTransition( + opacity: _opacity, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + widget.message, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } +}