дизайна поделал заглушек потыкал

This commit is contained in:
Jganenokk
2026-03-26 12:06:42 +07:00
parent 2149db96e4
commit f93a89b9f3
8 changed files with 927 additions and 316 deletions
+31
View File
@@ -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';
}
+64 -81
View File
@@ -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<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,
),
),
),
),
),
),
);
}
}
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<LoginScreen> {
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<LoginScreen> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {},
onPressed: () => _showSecurityOptions(context),
icon: Icon(
Symbols.admin_panel_settings,
color: cs.onSurfaceVariant,
@@ -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,
),
),
),
],
);
}
}
+21
View File
@@ -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,
),
),
);
}
}
+471 -235
View File
@@ -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<ChatListScreen> createState() => _ChatListScreenState();
}
class _ChatListScreenState extends State<ChatListScreen> {
class _ChatListScreenState extends State<ChatListScreen>
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<ChatListScreen> {
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<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,
),
],
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<ChatListScreen> {
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<ChatListScreen> {
),
),
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<ChatListScreen> {
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<ChatListScreen> {
),
);
}
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,
),
),
],
),
);
}
}
@@ -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,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,
),
),
);
}
}
@@ -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,
),
),
),
),
),
),
);
}
}