дизайна поделал заглушек потыкал
This commit is contained in:
@@ -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';
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user