Initial Commit

This commit is contained in:
ivan2282
2025-11-15 20:06:40 +03:00
commit 205d11df0d
233 changed files with 52572 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,440 @@
import 'package:flutter/material.dart';
import 'package:gwid/tos_screen.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutScreen extends StatelessWidget {
final bool isModal;
const AboutScreen({super.key, this.isModal = false});
Future<void> _launchUrl(String url) async {
if (!await launchUrl(Uri.parse(url))) {
print('Could not launch $url');
}
}
Widget _buildTeamMember(
BuildContext context, {
required String name,
required String role,
required String description,
}) {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
style: textTheme.bodyMedium?.copyWith(height: 1.5),
children: [
TextSpan(
text: '$name',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '$role'),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 2.0),
child: Text(
description,
style: textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (isModal) {
return buildModalContent(context);
}
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text("О нас"),
),
body: ListView(
padding: const EdgeInsets.all(24.0),
children: [
Text(
"Команда «Komet»",
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
"Мы — команда энтузиастов, создавшая Komet. Нас объединила страсть к технологиям и желание дать пользователям свободу выбора.",
style: TextStyle(fontSize: 16, height: 1.5),
),
const SizedBox(height: 24),
Card(
clipBehavior: Clip.antiAlias, // для скругления углов InkWell
child: ListTile(
leading: const Icon(Icons.description_outlined),
title: const Text("Пользовательское соглашение"),
subtitle: const Text("Правовая информация и условия"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const TosScreen()),
);
},
),
),
const SizedBox(height: 24),
Text(
"Наша команда:",
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildTeamMember(
context,
name: "Floppy",
role: "руководитель проекта",
description: "Стратегическое видение и общее руководство",
),
_buildTeamMember(
context,
name: "Klocky",
role: "главный программист",
description: "Архитектура и ключевые технические решения",
),
_buildTeamMember(
context,
name: "Noxzion",
role: "программист",
description: "Участие в разработке приложения и сайта",
),
_buildTeamMember(
context,
name: "Jganenok",
role: "программист",
description: "Участие в разработке и пользовательские интерфейсы",
),
_buildTeamMember(
context,
name: "Zennix",
role: "программист",
description: "Участие в разработке и технические решения",
),
_buildTeamMember(
context,
name: "Qmark",
role: "программист",
description: "Участие в разработке и технические решения",
),
_buildTeamMember(
context,
name: "ivan2282",
role: "программист",
description: "Основной программист клиента на GNU/Linux",
),
_buildTeamMember(
context,
name: "Ink",
role: "документация сервера",
description: "Техническая документация и API",
),
_buildTeamMember(
context,
name: "Килобайт",
role: "веб-разработчик и дизайнер",
description: "Веб-платформа и дизайн-система",
),
_buildTeamMember(
context,
name: "WhiteMax",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
_buildTeamMember(
context,
name: "Mixott Orego",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
_buildTeamMember(
context,
name: "Raspberry",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
const SizedBox(height: 24),
const Text(
"Мы верим в открытость, прозрачность и право пользователей на выбор. Komet — это наш ответ излишним ограничениям.",
style: TextStyle(fontSize: 16, height: 1.5),
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
InkWell(
onTap: () => _launchUrl('https://t.me/TeamKomet'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: RichText(
text: TextSpan(
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(height: 1.5),
children: [
const TextSpan(text: "Связаться с нами: \n"),
TextSpan(
text: "Телеграм-канал: https://t.me/TeamKomet",
style: TextStyle(
color: colors.primary,
decoration: TextDecoration.underline,
decorationColor: colors.primary,
),
),
],
),
),
),
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Column(
children: [
Image.asset(
'assets/icon/komet.png',
width: 128,
height: 128,
),
const SizedBox(height: 16),
Text(
'Komet',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
const SizedBox(height: 8),
Text(
'Версия 0.3.0',
style: TextStyle(
fontSize: 16,
color: colors.onSurface.withOpacity(0.7),
),
),
],
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Команда разработки',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
const SizedBox(height: 12),
Text(
'Мы — команда энтузиастов, создавшая Komet. Нас объединила страсть к технологиям и желание дать пользователям свободу выбора.',
style: TextStyle(
color: colors.onSurface.withOpacity(0.8),
height: 1.5,
),
),
const SizedBox(height: 16),
Text(
'Наша команда:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
const SizedBox(height: 12),
_buildTeamMember(
context,
name: "Floppy",
role: "руководитель проекта",
description: "Стратегическое видение и общее руководство",
),
_buildTeamMember(
context,
name: "Klocky",
role: "главный программист",
description: "Архитектура и ключевые технические решения",
),
_buildTeamMember(
context,
name: "Noxzion",
role: "программист",
description: "Участие в разработке приложения и сайта",
),
_buildTeamMember(
context,
name: "Jganenok",
role: "программист",
description: "Участие в разработке и пользовательские интерфейсы",
),
_buildTeamMember(
context,
name: "Zennix",
role: "программист",
description: "Участие в разработке и технические решения",
),
_buildTeamMember(
context,
name: "Qmark",
role: "программист",
description: "Участие в разработке и технические решения",
),
_buildTeamMember(
context,
name: "Ink",
role: "документация сервера",
description: "Техническая документация и API",
),
_buildTeamMember(
context,
name: "Килобайт",
role: "веб-разработчик и дизайнер",
description: "Веб-платформа и дизайн-система",
),
_buildTeamMember(
context,
name: "WhiteMax",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
_buildTeamMember(
context,
name: "Mixott Orego",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
_buildTeamMember(
context,
name: "Raspberry",
role: "PR-менеджер",
description: "Коммуникация с сообществом и продвижение проекта",
),
const SizedBox(height: 16),
Text(
'Мы верим в открытость, прозрачность и право пользователей на выбор. Komet — это наш ответ излишним ограничениям.',
style: TextStyle(
color: colors.onSurface.withOpacity(0.8),
height: 1.5,
),
),
const SizedBox(height: 16),
InkWell(
onTap: () => _launchUrl('https://t.me/TeamKomet'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: RichText(
text: TextSpan(
style: TextStyle(
color: colors.onSurface.withOpacity(0.8),
height: 1.5,
),
children: [
const TextSpan(text: "Связаться с нами: \n"),
TextSpan(
text: "Телеграм-канал: https://t.me/TeamKomet",
style: TextStyle(
color: colors.primary,
decoration: TextDecoration.underline,
decorationColor: colors.primary,
),
),
],
),
),
),
),
],
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Полезные ссылки',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.description_outlined),
title: const Text('Пользовательское соглашение'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const TosScreen(),
),
);
},
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,370 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
class AnimationsScreen extends StatelessWidget {
const AnimationsScreen({super.key});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final theme = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(
title: const Text("Настройки анимаций"),
backgroundColor: colors.surface,
surfaceTintColor: Colors.transparent,
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
children: [
_ModernSection(
title: "Анимации сообщений",
children: [
_DropdownSettingTile(
icon: Icons.chat_bubble_outline,
title: "Стиль появления",
items: TransitionOption.values,
value: theme.messageTransition,
onChanged: (value) {
if (value != null) theme.setMessageTransition(value);
},
itemToString: (item) => item.displayName,
),
const Divider(height: 24),
_CustomSettingTile(
icon: Icons.photo_library_outlined,
title: "Анимация фото",
subtitle: "Плавное появление фото в чате",
child: Switch(
value: theme.animatePhotoMessages,
onChanged: (value) => theme.setAnimatePhotoMessages(value),
),
),
if (theme.messageTransition == TransitionOption.slide) ...[
const SizedBox(height: 8),
_SliderTile(
icon: Icons.open_in_full_rounded,
label: "Расстояние слайда",
value: theme.messageSlideDistance,
min: 1.0,
max: 200.0,
divisions: 20,
onChanged: (value) => theme.setMessageSlideDistance(value),
displayValue: "${theme.messageSlideDistance.round()}px",
),
],
],
),
const SizedBox(height: 24),
_ModernSection(
title: "Переходы и эффекты",
children: [
_DropdownSettingTile(
icon: Icons.swap_horiz_rounded,
title: "Переход между чатами",
items: TransitionOption.values,
value: theme.chatTransition,
onChanged: (value) {
if (value != null) theme.setChatTransition(value);
},
itemToString: (item) => item.displayName,
),
const Divider(height: 24),
_DropdownSettingTile(
icon: Icons.auto_awesome_motion_outlined,
title: "Дополнительные эффекты",
subtitle: "Для диалогов и других элементов",
items: TransitionOption.values,
value: theme.extraTransition,
onChanged: (value) {
if (value != null) theme.setExtraTransition(value);
},
itemToString: (item) => item.displayName,
),
if (theme.extraTransition == TransitionOption.slide) ...[
const SizedBox(height: 8),
_SliderTile(
icon: Icons.bolt_rounded,
label: "Сила эффекта",
value: theme.extraAnimationStrength,
min: 1.0,
max: 400.0,
divisions: 20,
onChanged: (value) => theme.setExtraAnimationStrength(value),
displayValue: "${theme.extraAnimationStrength.round()}",
),
],
],
),
const SizedBox(height: 24),
_ModernSection(
title: "Управление",
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 4,
),
leading: Icon(Icons.restore_rounded, color: colors.error),
title: Text(
"Сбросить настройки анимаций",
style: TextStyle(color: colors.error),
),
subtitle: const Text("Вернуть все значения по умолчанию"),
onTap: () => _showResetDialog(context, theme),
),
],
),
],
),
);
}
void _showResetDialog(BuildContext context, ThemeProvider theme) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('Сбросить настройки?'),
content: const Text(
'Все параметры анимаций на этом экране будут возвращены к значениям по умолчанию.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'),
),
FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed: () {
theme.resetAnimationsToDefault();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Настройки анимаций сброшены'),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
);
},
icon: const Icon(Icons.restore),
label: const Text('Сбросить'),
),
],
),
);
}
}
class _ModernSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _ModernSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, bottom: 12.0),
child: Text(
title.toUpperCase(),
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
letterSpacing: 0.8,
),
),
),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant.withOpacity(0.3)),
),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Column(children: children),
),
),
],
);
}
}
class _CustomSettingTile extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget child;
const _CustomSettingTile({
required this.icon,
required this.title,
this.subtitle,
required this.child,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
if (subtitle != null)
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
child,
],
),
);
}
}
class _DropdownSettingTile<T> extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final T value;
final List<T> items;
final ValueChanged<T?> onChanged;
final String Function(T) itemToString;
const _DropdownSettingTile({
required this.icon,
required this.title,
this.subtitle,
required this.value,
required this.items,
required this.onChanged,
required this.itemToString,
});
@override
Widget build(BuildContext context) {
return _CustomSettingTile(
icon: icon,
title: title,
subtitle: subtitle,
child: DropdownButton<T>(
value: value,
underline: const SizedBox.shrink(),
onChanged: onChanged,
items: items.map((item) {
return DropdownMenuItem(value: item, child: Text(itemToString(item)));
}).toList(),
),
);
}
}
class _SliderTile extends StatelessWidget {
final IconData? icon;
final String label;
final double value;
final double min;
final double max;
final int divisions;
final ValueChanged<double> onChanged;
final String displayValue;
const _SliderTile({
this.icon,
required this.label,
required this.value,
required this.min,
required this.max,
required this.divisions,
required this.onChanged,
required this.displayValue,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (icon != null) ...[
Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
],
Expanded(
child: Text(label, style: const TextStyle(fontSize: 14)),
),
Text(
displayValue,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(
height: 30,
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
import 'package:gwid/screens/settings/customization_screen.dart';
import 'package:gwid/screens/settings/animations_screen.dart';
class AppearanceSettingsScreen extends StatelessWidget {
final bool isModal;
const AppearanceSettingsScreen({super.key, this.isModal = false});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeProvider>();
final colors = Theme.of(context).colorScheme;
if (isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(title: const Text("Внешний вид")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Кастомизация", colors),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.palette_outlined),
title: const Text("Настройки тем"),
subtitle: const Text("Тема, обои и другие настройки"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CustomizationScreen(),
),
);
},
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.animation),
title: const Text("Настройки анимаций"),
subtitle: const Text("Анимации сообщений и переходов"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => AnimationsScreen(),
),
);
},
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Производительность", colors),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.speed_outlined),
title: const Text("Оптимизация чатов"),
subtitle: const Text("Улучшить производительность в чатах"),
value: theme.optimizeChats,
onChanged: (value) => theme.setOptimizeChats(value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.flash_on_outlined),
title: const Text("Ультра оптимизация"),
subtitle: const Text("Максимальная производительность"),
value: theme.ultraOptimizeChats,
onChanged: (value) => theme.setUltraOptimizeChats(value),
),
],
),
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
final theme = context.watch<ThemeProvider>();
final colors = Theme.of(context).colorScheme;
return ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Кастомизация", colors),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.palette_outlined),
title: const Text("Настройки тем"),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CustomizationScreen(),
),
);
},
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.animation_outlined),
title: const Text("Анимации"),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AnimationsScreen(),
),
);
},
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Производительность", colors),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.speed_outlined),
title: const Text("Ультра-оптимизация чатов"),
subtitle: const Text("Максимальная производительность"),
value: theme.ultraOptimizeChats,
onChanged: (value) => theme.setUltraOptimizeChats(value),
),
],
),
),
],
);
}
Widget _buildModalSettings(BuildContext context, ThemeProvider theme, ColorScheme colors) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Внешний вид",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Кастомизация", colors),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.palette_outlined),
title: const Text("Настройки тем"),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CustomizationScreen(),
),
);
},
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.animation_outlined),
title: const Text("Анимации"),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AnimationsScreen(),
),
);
},
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Производительность", colors),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.speed_outlined),
title: const Text("Ультра-оптимизация чатов"),
subtitle: const Text("Максимальная производительность"),
value: theme.ultraOptimizeChats,
onChanged: (value) => theme.setUltraOptimizeChats(value),
),
],
),
),
],
),
),
],
),
),
),
],
),
);
}
Widget _buildSectionTitle(String title, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
);
}
}
class _OutlinedSection extends StatelessWidget {
final Widget child;
const _OutlinedSection({required this.child});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: colors.outline.withOpacity(0.3)),
borderRadius: BorderRadius.circular(12),
),
child: child,
);
}
}

View File

@@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
class BypassScreen extends StatelessWidget {
final bool isModal;
const BypassScreen({super.key, this.isModal = false});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(title: const Text("Bypass")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: colors.primary),
const SizedBox(width: 8),
Text(
"Обход блокировки",
style: TextStyle(
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
],
),
const SizedBox(height: 8),
Text(
"Эта функция позволяет отправлять сообщения заблокированным пользователям, "
"даже если они заблокировали вас. Включите эту опцию, если хотите обойти "
"стандартные ограничения мессенджера.",
style: TextStyle(color: colors.onSurfaceVariant),
),
],
),
),
const SizedBox(height: 24),
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return Card(
child: SwitchListTile(
title: const Text(
"Обход блокировки",
style: TextStyle(fontWeight: FontWeight.w600),
),
subtitle: const Text(
"Разрешить отправку сообщений заблокированным пользователям",
),
value: themeProvider.blockBypass,
onChanged: (value) {
themeProvider.setBlockBypass(value);
},
secondary: Icon(
themeProvider.blockBypass
? Icons.psychology
: Icons.psychology_outlined,
color: themeProvider.blockBypass
? colors.primary
: colors.onSurfaceVariant,
),
),
);
},
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outline.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_outlined,
color: colors.primary,
size: 16,
),
const SizedBox(width: 8),
Text(
"Важно знать",
style: TextStyle(
fontWeight: FontWeight.w600,
color: colors.primary,
),
),
],
),
const SizedBox(height: 8),
Text(
"Используя любую из bypass функций мы не несем ответственности за ваш аккаунт",
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 14,
),
),
],
),
),
],
),
);
}
Widget _buildModalSettings(BuildContext context, ColorScheme colors) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Bypass",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colors.outline.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: colors.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
"Информация",
style: TextStyle(
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
],
),
const SizedBox(height: 8),
Text(
"Эта функция предназначена для обхода ограничений и блокировок. Используйте с осторожностью и только в законных целях.",
style: TextStyle(
color: colors.onSurface.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
const SizedBox(height: 20),
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return SwitchListTile(
title: const Text("Включить обход"),
subtitle: const Text("Активировать функции обхода ограничений"),
value: false, // Временно отключено
onChanged: (value) {
},
);
},
),
],
),
),
],
),
),
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colors.outline.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: colors.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
"Информация",
style: TextStyle(
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
],
),
const SizedBox(height: 8),
Text(
"Эта функция предназначена для обхода ограничений и блокировок. Используйте с осторожностью. Всю ответственность за ваш аккаунт несете только вы.",
style: TextStyle(
color: colors.onSurface.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
const SizedBox(height: 20),
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return SwitchListTile(
title: const Text("Обход блокировки"),
subtitle: const Text("Активировать функции обхода ограничений"),
value: themeProvider.blockBypass,
onChanged: (value) {
themeProvider.setBlockBypass(value);
},
);
},
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_saver/file_saver.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/spoofing_service.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:crypto/crypto.dart' as crypto;
class ExportSessionScreen extends StatefulWidget {
const ExportSessionScreen({super.key});
@override
State<ExportSessionScreen> createState() => _ExportSessionScreenState();
}
class _ExportSessionScreenState extends State<ExportSessionScreen> {
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isExporting = false;
bool _saveProxySettings = false;
Future<void> _exportAndSaveSession() async {
if (!mounted) return;
setState(() => _isExporting = true);
final messenger = ScaffoldMessenger.of(context);
try {
final spoofData = await SpoofingService.getSpoofedSessionData();
final token = ApiService.instance.token;
if (token == null || token.isEmpty) {
throw Exception('Токен пользователя не найден.');
}
final sessionData = <String, dynamic>{
'token': token,
'spoof_data': spoofData ?? 'Подмена устройства неактивна',
};
if (_saveProxySettings) {
final proxySettings = await ProxyService.instance.loadProxySettings();
sessionData['proxy_settings'] = proxySettings.toJson();
}
const jsonEncoder = JsonEncoder.withIndent(' ');
final plainJsonContent = jsonEncoder.convert(sessionData);
String finalFileContent;
final password = _passwordController.text;
if (password.isNotEmpty) {
final keyBytes = utf8.encode(password);
final keyHash = crypto.sha256.convert(keyBytes);
final key = encrypt.Key(Uint8List.fromList(keyHash.bytes));
final iv = encrypt.IV.fromLength(16);
final encrypter = encrypt.Encrypter(
encrypt.AES(key, mode: encrypt.AESMode.cbc),
);
final encrypted = encrypter.encrypt(plainJsonContent, iv: iv);
final encryptedOutput = {
'encrypted': true,
'iv_base64': iv.base64,
'data_base64': encrypted.base64,
};
finalFileContent = jsonEncoder.convert(encryptedOutput);
} else {
finalFileContent = plainJsonContent;
}
Uint8List bytes = Uint8List.fromList(utf8.encode(finalFileContent));
String? filePath = await FileSaver.instance.saveAs(
name: 'komet_session_${DateTime.now().millisecondsSinceEpoch}',
bytes: bytes,
fileExtension: 'json',
mimeType: MimeType.json,
);
if (filePath != null && mounted) {
messenger.showSnackBar(
const SnackBar(
content: Text('Файл сессии успешно сохранен'),
backgroundColor: Colors.green,
),
);
} else if (mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('Сохранение файла было отменено.')),
);
}
} catch (e) {
messenger.showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('Не удалось экспортировать сессию: $e'),
),
);
} finally {
if (mounted) {
setState(() => _isExporting = false);
}
}
}
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Экспорт сессии')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: CircleAvatar(
radius: 40,
backgroundColor: colors.primaryContainer,
child: Icon(
Icons.upload_file_outlined,
size: 40,
color: colors.onPrimaryContainer,
),
),
),
const SizedBox(height: 16),
Text(
'Резервная копия сессии',
textAlign: TextAlign.center,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Создайте зашифрованный файл для переноса вашего аккаунта на другое устройство без повторной авторизации.',
textAlign: TextAlign.center,
style: textTheme.bodyLarge?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 24),
Text(
'1. Защитите файл паролем',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Настоятельно рекомендуется установить пароль для шифрования (AES-256). Без него файл будет сохранен в открытом виде.',
style: textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Пароль (необязательно)',
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () =>
setState(() => _isPasswordVisible = !_isPasswordVisible),
),
),
),
const SizedBox(height: 24),
Text(
'2. Дополнительные данные',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Card(
margin: EdgeInsets.zero,
child: CheckboxListTile(
title: const Text('Сохранить настройки прокси'),
subtitle: const Text(
'Включить текущие параметры прокси в файл экспорта.',
),
value: _saveProxySettings,
onChanged: (bool? value) =>
setState(() => _saveProxySettings = value ?? false),
controlAffinity:
ListTileControlAffinity.leading, // Чекбокс слева
),
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.warning_amber_rounded,
color: colors.error,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Никогда и никому не передавайте этот файл. Он дает полный доступ к вашему аккаунту.',
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: _isExporting ? null : _exportAndSaveSession,
icon: _isExporting
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: const Icon(Icons.download_for_offline_outlined),
label: Text(
_isExporting ? 'Сохранение...' : 'Экспортировать и сохранить',
),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,521 @@
import 'package:flutter/material.dart';
import 'package:disable_battery_optimization/disable_battery_optimization.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io' show Platform;
class KometMiscScreen extends StatefulWidget {
final bool isModal;
const KometMiscScreen({super.key, this.isModal = false});
@override
State<KometMiscScreen> createState() => _KometMiscScreenState();
}
class _KometMiscScreenState extends State<KometMiscScreen> {
bool? _isBatteryOptimizationDisabled;
bool _isAutoUpdateEnabled = true;
bool _showUpdateNotification = true;
bool _enableWebVersionCheck = false;
@override
void initState() {
super.initState();
_checkBatteryOptimizationStatus();
_loadUpdateSettings();
}
Future<void> _loadUpdateSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_isAutoUpdateEnabled = prefs.getBool('auto_update_enabled') ?? true;
_showUpdateNotification =
prefs.getBool('show_update_notification') ?? true;
_enableWebVersionCheck =
prefs.getBool('enable_web_version_check') ?? false;
});
}
Future<void> _checkBatteryOptimizationStatus() async {
bool? isDisabled =
await DisableBatteryOptimization.isBatteryOptimizationDisabled;
if (mounted) {
setState(() {
_isBatteryOptimizationDisabled = isDisabled;
});
}
}
Future<void> _requestDisableBatteryOptimization() async {
await DisableBatteryOptimization.showDisableBatteryOptimizationSettings();
Future.delayed(const Duration(milliseconds: 500), () {
_checkBatteryOptimizationStatus();
});
}
Future<void> _updateSettings(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, value);
}
@override
Widget build(BuildContext context) {
String subtitleText;
Color statusColor;
final defaultTextColor = Theme.of(context).textTheme.bodyMedium?.color;
final isDesktopOrIOS =
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isIOS;
if (isDesktopOrIOS) {
subtitleText = "Недоступно";
statusColor = Colors.grey;
} else if (_isBatteryOptimizationDisabled == null) {
subtitleText = "Проверка статуса...";
statusColor = Colors.grey;
} else if (_isBatteryOptimizationDisabled == true) {
subtitleText = "Разрешено";
statusColor = Colors.green;
} else {
subtitleText = "Не разрешено";
statusColor = Colors.orange;
}
if (widget.isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(title: const Text("Komet Misc")),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(
Icons.battery_charging_full_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Фоновая работа"),
subtitle: RichText(
text: TextSpan(
style: TextStyle(fontSize: 14, color: defaultTextColor),
children: <TextSpan>[
const TextSpan(text: 'Статус: '),
TextSpan(
text: subtitleText,
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
),
),
onTap: isDesktopOrIOS ? null : _requestDisableBatteryOptimization,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
isDesktopOrIOS
? 'Фоновая работа недоступна на данной платформе.'
: 'Для стабильной работы приложения в фоновом режиме рекомендуется отключить оптимизацию расхода заряда батареи.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontSize: 12,
),
),
),
const Divider(height: 20),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: Column(
children: [
SwitchListTile(
secondary: Icon(
Icons.wifi_find_outlined,
color: _enableWebVersionCheck
? Theme.of(context).colorScheme.primary
: Colors.grey,
),
title: const Text("Проверка версии через web"),
subtitle: Text(
_enableWebVersionCheck
? "Проверяет актуальную версию на web.max.ru"
: "Проверка версии отключена",
),
value: _enableWebVersionCheck,
onChanged: (bool value) {
setState(() {
_enableWebVersionCheck = value;
});
_updateSettings('enable_web_version_check', value);
},
),
const Divider(height: 1),
SwitchListTile(
secondary: Icon(
Icons.system_update_alt_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Автообновление сессии"),
subtitle: const Text("Версия будет обновляться в фоне"),
value: _isAutoUpdateEnabled,
onChanged: (bool value) {
setState(() {
_isAutoUpdateEnabled = value;
});
_updateSettings('auto_update_enabled', value);
},
),
SwitchListTile(
secondary: Icon(
Icons.notifications_active_outlined,
color: _isAutoUpdateEnabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
),
title: const Text("Уведомлять о новой версии"),
subtitle: Text(
_isAutoUpdateEnabled
? "Недоступно при автообновлении"
: "Показывать диалог при запуске",
),
value: _showUpdateNotification,
onChanged: _isAutoUpdateEnabled
? null
: (bool value) {
setState(() {
_showUpdateNotification = value;
});
_updateSettings('show_update_notification', value);
},
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
'Автообновление автоматически изменит версию вашей сессии на последнюю доступную без дополнительных уведомлений.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontSize: 12,
),
),
),
],
),
);
}
Widget _buildModalSettings(
BuildContext context,
String subtitleText,
Color statusColor,
Color? defaultTextColor,
) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Komet Misc",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.battery_charging_full),
title: const Text("Оптимизация батареи"),
subtitle: Text(subtitleText),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.circle,
color: statusColor,
size: 12,
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right),
],
),
onTap:
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isIOS
? null
: _requestDisableBatteryOptimization,
),
),
const SizedBox(height: 16),
Card(
child: Column(
children: [
SwitchListTile(
title: const Text("Автообновления"),
subtitle: const Text(
"Автоматически проверять обновления",
),
value: _isAutoUpdateEnabled,
onChanged: (value) {
setState(() {
_isAutoUpdateEnabled = value;
});
_updateSettings('auto_update_enabled', value);
},
),
SwitchListTile(
title: const Text("Уведомления об обновлениях"),
subtitle: const Text(
"Показывать уведомления о доступных обновлениях",
),
value: _showUpdateNotification,
onChanged: (value) {
setState(() {
_showUpdateNotification = value;
});
_updateSettings(
'show_update_notification',
value,
);
},
),
SwitchListTile(
title: const Text("Проверка веб-версии"),
subtitle: const Text(
"Проверять обновления через веб-интерфейс",
),
value: _enableWebVersionCheck,
onChanged: (value) {
setState(() {
_enableWebVersionCheck = value;
});
_updateSettings(
'enable_web_version_check',
value,
);
},
),
],
),
),
],
),
),
],
),
),
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
String subtitleText;
Color statusColor;
final defaultTextColor = Theme.of(context).textTheme.bodyMedium?.color;
final isDesktopOrIOS =
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isIOS;
if (isDesktopOrIOS) {
subtitleText = "Недоступно";
statusColor = Colors.grey;
} else if (_isBatteryOptimizationDisabled == null) {
subtitleText = "Проверка статуса...";
statusColor = Colors.grey;
} else if (_isBatteryOptimizationDisabled == true) {
subtitleText = "Разрешено";
statusColor = Colors.green;
} else {
subtitleText = "Не разрешено";
statusColor = Colors.orange;
}
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(
Icons.battery_charging_full_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Фоновая работа"),
subtitle: RichText(
text: TextSpan(
style: TextStyle(fontSize: 14, color: defaultTextColor),
children: <TextSpan>[
const TextSpan(text: 'Статус: '),
TextSpan(
text: subtitleText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: isDesktopOrIOS ? null : _requestDisableBatteryOptimization,
),
),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: Column(
children: [
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
secondary: Icon(
Icons.system_update_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Автообновления"),
subtitle: const Text(
"Автоматически проверять и устанавливать обновления",
),
value: _isAutoUpdateEnabled,
onChanged: (value) {
setState(() {
_isAutoUpdateEnabled = value;
});
_updateSettings('auto_update_enabled', value);
},
),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
secondary: Icon(
Icons.notifications_active_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Уведомления об обновлениях"),
subtitle: const Text(
"Показывать уведомления о доступных обновлениях",
),
value: _showUpdateNotification,
onChanged: (value) {
setState(() {
_showUpdateNotification = value;
});
_updateSettings('show_update_notification', value);
},
),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
secondary: Icon(
Icons.web_rounded,
color: Theme.of(context).colorScheme.primary,
),
title: const Text("Проверка веб-версии"),
subtitle: const Text(
"Проверять обновления веб-версии приложения",
),
value: _enableWebVersionCheck,
onChanged: (value) {
setState(() {
_enableWebVersionCheck = value;
});
_updateSettings('enable_web_version_check', value);
},
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,868 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math';
import 'package:gwid/api_service.dart';
class NetworkScreen extends StatefulWidget {
const NetworkScreen({super.key});
@override
State<NetworkScreen> createState() => _NetworkScreenState();
}
class _NetworkScreenState extends State<NetworkScreen>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
NetworkStats? _networkStats;
bool _isLoading = true;
Timer? _updateTimer;
bool _isMonitoring = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_loadNetworkStats();
}
@override
void dispose() {
_animationController.dispose();
_updateTimer?.cancel();
super.dispose();
}
Future<void> _loadNetworkStats() async {
try {
final stats = await _getNetworkStats();
setState(() {
_networkStats = stats;
_isLoading = false;
});
_animationController.forward();
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
Future<NetworkStats> _getNetworkStats() async {
final stats = await ApiService.instance.getNetworkStatistics();
final totalDailyTraffic = stats['totalTraffic'] as double;
final messagesTraffic = stats['messagesTraffic'] as double;
final mediaTraffic = stats['mediaTraffic'] as double;
final syncTraffic = stats['syncTraffic'] as double;
final otherTraffic = stats['otherTraffic'] as double;
final currentSpeed = stats['currentSpeed'] as double;
final hourlyData = stats['hourlyStats'] as List<dynamic>;
final hourlyStats = List.generate(24, (index) {
if (index < hourlyData.length) {
return HourlyStats(hour: index, traffic: hourlyData[index] as double);
}
final hour = index;
final isActive = hour >= 8 && hour <= 23;
final baseTraffic = isActive ? 20.0 * 1024 * 1024 : 2.0 * 1024 * 1024;
return HourlyStats(hour: hour, traffic: baseTraffic);
});
return NetworkStats(
totalDailyTraffic: totalDailyTraffic,
messagesTraffic: messagesTraffic,
mediaTraffic: mediaTraffic,
syncTraffic: syncTraffic,
otherTraffic: otherTraffic,
currentSpeed: currentSpeed,
hourlyStats: hourlyStats,
isConnected: stats['isConnected'] as bool,
connectionType: stats['connectionType'] as String,
signalStrength: stats['signalStrength'] as int,
ping: stats['ping'] as int,
jitter: stats['jitter'] as double,
packetLoss: stats['packetLoss'] as double,
);
}
void _startMonitoring() {
if (_isMonitoring) return;
setState(() {
_isMonitoring = true;
});
_updateTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
_loadNetworkStats();
});
}
void _stopMonitoring() {
_updateTimer?.cancel();
setState(() {
_isMonitoring = false;
});
}
void _resetStats() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Сбросить статистику'),
content: const Text(
'Это действие сбросит всю статистику использования сети. '
'Это действие нельзя отменить.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Сбросить'),
),
],
),
);
if (confirmed == true) {
await _loadNetworkStats();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Статистика сброшена'),
backgroundColor: Colors.green,
),
);
}
}
}
String _formatBytes(double bytes) {
if (bytes < 1024) return '${bytes.toStringAsFixed(0)} B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024)
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
String _formatSpeed(double bytesPerSecond) {
if (bytesPerSecond < 1024)
return '${bytesPerSecond.toStringAsFixed(0)} B/s';
if (bytesPerSecond < 1024 * 1024)
return '${(bytesPerSecond / 1024).toStringAsFixed(1)} KB/s';
return '${(bytesPerSecond / (1024 * 1024)).toStringAsFixed(1)} MB/s';
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Сеть'),
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
elevation: 0,
actions: [
IconButton(
icon: Icon(_isMonitoring ? Icons.pause : Icons.play_arrow),
onPressed: _isMonitoring ? _stopMonitoring : _startMonitoring,
tooltip: _isMonitoring
? 'Остановить мониторинг'
: 'Начать мониторинг',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _networkStats == null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.wifi_off,
size: 64,
color: colors.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'Не удалось загрузить статистику сети',
style: TextStyle(
color: colors.onSurface.withOpacity(0.6),
fontSize: 16,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadNetworkStats,
child: const Text('Повторить'),
),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildConnectionStatus(colors),
const SizedBox(height: 24),
_buildNetworkChart(colors),
const SizedBox(height: 24),
_buildCurrentSpeed(colors),
const SizedBox(height: 24),
_buildTrafficDetails(colors),
const SizedBox(height: 24),
_buildHourlyChart(colors),
const SizedBox(height: 24),
_buildActionButtons(colors),
],
),
),
);
}
Widget _buildConnectionStatus(ColorScheme colors) {
final stats = _networkStats!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: stats.isConnected
? colors.primary.withOpacity(0.1)
: colors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
stats.isConnected ? Icons.wifi : Icons.wifi_off,
color: stats.isConnected ? colors.primary : colors.error,
size: 30,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stats.isConnected ? 'Подключено' : 'Отключено',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 4),
Text(
stats.connectionType,
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withOpacity(0.7),
),
),
if (stats.isConnected) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.signal_cellular_alt,
size: 16,
color: colors.primary,
),
const SizedBox(width: 4),
Text(
'${stats.signalStrength}%',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withOpacity(0.6),
),
),
],
),
],
],
),
),
],
),
);
}
Widget _buildNetworkChart(ColorScheme colors) {
final stats = _networkStats!;
final totalTraffic = stats.totalDailyTraffic;
final usagePercentage = totalTraffic > 0
? (stats.messagesTraffic +
stats.mediaTraffic +
stats.syncTraffic +
stats.otherTraffic) /
totalTraffic
: 0.0;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
children: [
Text(
'Использование сети за день',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 24),
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return SizedBox(
width: 200,
height: 200,
child: Stack(
children: [
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.surfaceContainerHighest,
),
),
CustomPaint(
size: const Size(200, 200),
painter: NetworkChartPainter(
progress: usagePercentage * _animation.value,
colors: colors,
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatBytes(totalTraffic),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
Text(
'использовано',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withOpacity(0.7),
),
),
],
),
),
],
),
);
},
),
const SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildLegendItem(
'Медиа',
_formatBytes(stats.mediaTraffic),
colors.primary,
),
_buildLegendItem(
'Сообщения',
_formatBytes(stats.messagesTraffic),
colors.secondary,
),
_buildLegendItem(
'Синхронизация',
_formatBytes(stats.syncTraffic),
colors.tertiary,
),
_buildLegendItem(
'Другое',
_formatBytes(stats.otherTraffic),
colors.outline,
),
],
),
],
),
);
}
Widget _buildLegendItem(String label, String value, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12)),
Text(
value,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500),
),
],
),
],
);
}
Widget _buildCurrentSpeed(ColorScheme colors) {
final stats = _networkStats!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.speed, color: colors.primary, size: 20),
const SizedBox(width: 8),
Text(
'Текущая скорость',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Text(
_formatSpeed(stats.currentSpeed),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: colors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'',
style: TextStyle(
fontSize: 12,
color: colors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
);
}
Widget _buildTrafficDetails(ColorScheme colors) {
final stats = _networkStats!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Детали трафика',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
_buildTrafficItem(
'Медиафайлы',
_formatBytes(stats.mediaTraffic),
Icons.photo_library_outlined,
colors.primary,
(stats.mediaTraffic / stats.totalDailyTraffic),
),
_buildTrafficItem(
'Сообщения',
_formatBytes(stats.messagesTraffic),
Icons.message_outlined,
colors.secondary,
(stats.messagesTraffic / stats.totalDailyTraffic),
),
_buildTrafficItem(
'Синхронизация',
_formatBytes(stats.syncTraffic),
Icons.sync,
colors.tertiary,
(stats.syncTraffic / stats.totalDailyTraffic),
),
_buildTrafficItem(
'Другие данные',
_formatBytes(stats.otherTraffic),
Icons.folder_outlined,
colors.outline,
(stats.otherTraffic / stats.totalDailyTraffic),
),
],
),
);
}
Widget _buildTrafficItem(
String title,
String size,
IconData icon,
Color color,
double percentage,
) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
),
const SizedBox(height: 2),
Text(
size,
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withOpacity(0.6),
),
),
],
),
),
Container(
width: 60,
height: 4,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: percentage,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
),
);
}
Widget _buildHourlyChart(ColorScheme colors) {
final stats = _networkStats!;
final maxTraffic = stats.hourlyStats
.map((e) => e.traffic)
.reduce((a, b) => a > b ? a : b);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Активность по часам',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: stats.hourlyStats.length,
itemBuilder: (context, index) {
final hourStats = stats.hourlyStats[index];
final height = maxTraffic > 0
? (hourStats.traffic / maxTraffic)
: 0.0;
return Container(
width: 20,
margin: const EdgeInsets.symmetric(horizontal: 2),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: height * 100,
decoration: BoxDecoration(
color: colors.primary.withOpacity(0.7),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 4),
Text(
'${hourStats.hour}',
style: TextStyle(
fontSize: 10,
color: colors.onSurface.withOpacity(0.6),
),
),
],
),
);
},
),
),
],
),
);
}
Widget _buildActionButtons(ColorScheme colors) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Действия',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _isMonitoring ? _stopMonitoring : _startMonitoring,
icon: Icon(_isMonitoring ? Icons.pause : Icons.play_arrow),
label: Text(
_isMonitoring ? 'Остановить мониторинг' : 'Начать мониторинг',
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _resetStats,
icon: const Icon(Icons.refresh),
label: const Text('Сбросить статистику'),
style: ElevatedButton.styleFrom(
backgroundColor: colors.error,
foregroundColor: colors.onError,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
);
}
}
class NetworkStats {
final double totalDailyTraffic;
final double messagesTraffic;
final double mediaTraffic;
final double syncTraffic;
final double otherTraffic;
final double currentSpeed;
final List<HourlyStats> hourlyStats;
final bool isConnected;
final String connectionType;
final int signalStrength;
final int ping;
final double jitter;
final double packetLoss;
NetworkStats({
required this.totalDailyTraffic,
required this.messagesTraffic,
required this.mediaTraffic,
required this.syncTraffic,
required this.otherTraffic,
required this.currentSpeed,
required this.hourlyStats,
required this.isConnected,
required this.connectionType,
required this.signalStrength,
this.ping = 25,
this.jitter = 2.5,
this.packetLoss = 0.01,
});
}
class HourlyStats {
final int hour;
final double traffic;
HourlyStats({required this.hour, required this.traffic});
}
class NetworkChartPainter extends CustomPainter {
final double progress;
final ColorScheme colors;
NetworkChartPainter({required this.progress, required this.colors});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 8;
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
paint.color = colors.surfaceContainerHighest;
canvas.drawCircle(center, radius, paint);
paint.color = colors.primary;
final sweepAngle = 2 * pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2, // Начинаем сверху
sweepAngle,
false,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is NetworkChartPainter &&
oldDelegate.progress != progress;
}
}

View File

@@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:gwid/screens/settings/network_screen.dart';
import 'package:gwid/screens/settings/proxy_settings_screen.dart';
import 'package:gwid/screens/settings/socket_log_screen.dart';
class NetworkSettingsScreen extends StatelessWidget {
final bool isModal;
const NetworkSettingsScreen({super.key, this.isModal = false});
@override
Widget build(BuildContext context) {
if (isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(title: const Text("Сеть")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildNetworkOption(
context,
icon: Icons.bar_chart_outlined,
title: "Мониторинг сети",
subtitle: "Просмотр статистики использования и скорости соединения",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const NetworkScreen()),
);
},
),
_buildNetworkOption(
context,
icon: Icons.shield_outlined,
title: "Настройки прокси",
subtitle: "Настроить подключение через прокси-сервер",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProxySettingsScreen(),
),
);
},
),
_buildNetworkOption(
context,
icon: Icons.history_outlined,
title: "Журнал WebSocket",
subtitle: "Просмотр логов подключения WebSocket для отладки",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SocketLogScreen(),
),
);
},
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_buildNetworkOption(
context,
icon: Icons.bar_chart_outlined,
title: "Мониторинг сети",
subtitle: "Статистика подключений и производительности",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NetworkScreen(),
),
);
},
),
const SizedBox(height: 16),
_buildNetworkOption(
context,
icon: Icons.vpn_key_outlined,
title: "Настройки прокси",
subtitle: "HTTP/HTTPS прокси, SOCKS",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProxySettingsScreen(),
),
);
},
),
const SizedBox(height: 16),
_buildNetworkOption(
context,
icon: Icons.list_alt_outlined,
title: "Логи сокетов",
subtitle: "Отладочная информация о соединениях",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SocketLogScreen(),
),
);
},
),
],
);
}
Widget _buildModalSettings(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Сеть",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildNetworkOption(
context,
icon: Icons.bar_chart_outlined,
title: "Мониторинг сети",
subtitle: "Статистика подключений и производительности",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NetworkScreen(),
),
);
},
),
const SizedBox(height: 16),
_buildNetworkOption(
context,
icon: Icons.vpn_key_outlined,
title: "Настройки прокси",
subtitle: "HTTP/HTTPS прокси, SOCKS",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProxySettingsScreen(),
),
);
},
),
const SizedBox(height: 16),
_buildNetworkOption(
context,
icon: Icons.list_alt_outlined,
title: "Логи сокетов",
subtitle: "Отладочная информация о соединениях",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SocketLogScreen(),
),
);
},
),
],
),
),
],
),
),
),
],
),
);
}
Widget _buildNetworkOption(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: onTap,
),
);
}
}

View File

@@ -0,0 +1,591 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'dart:io' show Platform;
class NotificationSettingsScreen extends StatefulWidget {
final bool isModal;
const NotificationSettingsScreen({super.key, this.isModal = false});
@override
State<NotificationSettingsScreen> createState() =>
_NotificationSettingsScreenState();
}
class _NotificationSettingsScreenState
extends State<NotificationSettingsScreen> {
String _chatsPushNotification = 'ON';
bool _mCallPushNotification = true;
bool _pushDetails = true;
String _chatsPushSound = 'DEFAULT';
String _pushSound = 'DEFAULT';
bool _isLoading = false;
Widget buildModalContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final isDesktopOrIOS =
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isIOS;
if (isDesktopOrIOS) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.info_outline, size: 48, color: colors.primary),
const SizedBox(height: 16),
Text(
'Фоновые уведомления недоступны',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colors.primary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
Platform.isIOS
? 'На iOS фоновые уведомления не поддерживаются системой.'
: 'На настольных платформах (Windows, macOS, Linux) фоновые уведомления отключены.',
style: TextStyle(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
),
),
],
);
}
return _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.chat_bubble_outline),
title: const Text("Уведомления из чатов"),
value: _chatsPushNotification == 'ON',
onChanged: (value) =>
_updateNotificationSetting(chatsPush: value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.phone_outlined),
title: const Text("Уведомления о звонках"),
value: _mCallPushNotification,
onChanged: (value) =>
_updateNotificationSetting(mCallPush: value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.visibility_outlined),
title: const Text("Показывать текст"),
subtitle: const Text("Показывать превью сообщения"),
value: _pushDetails,
onChanged: (value) =>
_updateNotificationSetting(pushDetails: value),
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.music_note_outlined),
title: const Text("Звук в чатах"),
trailing: Text(
_getSoundDescription(_chatsPushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Звук уведомлений чатов",
_chatsPushSound,
(value) =>
_updateNotificationSetting(chatsSound: value),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.notifications_active_outlined),
title: const Text("Общий звук"),
trailing: Text(
_getSoundDescription(_pushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Общий звук уведомлений",
_pushSound,
(value) => _updateNotificationSetting(pushSound: value),
),
),
],
),
),
],
);
}
@override
void initState() {
super.initState();
_loadCurrentSettings();
}
Future<void> _loadCurrentSettings() async {
setState(() => _isLoading = true);
await Future.delayed(
const Duration(milliseconds: 500),
); // Имитация загрузки
setState(() => _isLoading = false);
}
Future<void> _updateNotificationSetting({
bool? chatsPush,
bool? mCallPush,
bool? pushDetails,
String? chatsSound,
String? pushSound,
}) async {
try {
await ApiService.instance.updatePrivacySettings(
chatsPushNotification: chatsPush,
mCallPushNotification: mCallPush,
pushDetails: pushDetails,
chatsPushSound: chatsSound,
pushSound: pushSound,
);
if (chatsPush != null)
setState(() => _chatsPushNotification = chatsPush ? 'ON' : 'OFF');
if (mCallPush != null) setState(() => _mCallPushNotification = mCallPush);
if (pushDetails != null) setState(() => _pushDetails = pushDetails);
if (chatsSound != null) setState(() => _chatsPushSound = chatsSound);
if (pushSound != null) setState(() => _pushSound = pushSound);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Настройки уведомлений обновлены'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка обновления: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
}
void _showSoundDialog(
String title,
String currentValue,
Function(String) onSelect,
) {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(title),
children: [
RadioListTile<String>(
title: const Text('Стандартный звук'),
value: 'DEFAULT',
groupValue: currentValue,
onChanged: (v) => Navigator.of(context).pop(v),
),
RadioListTile<String>(
title: const Text('Без звука'),
value: '_NONE_',
groupValue: currentValue,
onChanged: (v) => Navigator.of(context).pop(v),
),
],
);
},
).then((selectedValue) {
if (selectedValue != null) {
onSelect(selectedValue);
}
});
}
String _getSoundDescription(String sound) {
switch (sound) {
case 'DEFAULT':
return 'Стандартный';
case '_NONE_':
return 'Без звука';
default:
return 'Неизвестно';
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (widget.isModal) {
return buildModalContent(context);
}
final isDesktopOrIOS =
Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isIOS;
if (isDesktopOrIOS) {
return Scaffold(
appBar: AppBar(title: const Text('Уведомления')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.info_outline, size: 48, color: colors.primary),
const SizedBox(height: 16),
Text(
'Фоновые уведомления недоступны',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colors.primary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
Platform.isIOS
? 'На iOS фоновые уведомления не поддерживаются системой.'
: 'На настольных платформах (Windows, macOS, Linux) фоновые уведомления отключены.',
style: TextStyle(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Уведомления')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.chat_bubble_outline),
title: const Text("Уведомления из чатов"),
value: _chatsPushNotification == 'ON',
onChanged: (value) =>
_updateNotificationSetting(chatsPush: value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.phone_outlined),
title: const Text("Уведомления о звонках"),
value: _mCallPushNotification,
onChanged: (value) =>
_updateNotificationSetting(mCallPush: value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.visibility_outlined),
title: const Text("Показывать текст"),
subtitle: const Text("Показывать превью сообщения"),
value: _pushDetails,
onChanged: (value) =>
_updateNotificationSetting(pushDetails: value),
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.music_note_outlined),
title: const Text("Звук в чатах"),
trailing: Text(
_getSoundDescription(_chatsPushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Звук в чатах",
_chatsPushSound,
(value) =>
_updateNotificationSetting(chatsSound: value),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(
Icons.notifications_active_outlined,
),
title: const Text("Общий звук"),
trailing: Text(
_getSoundDescription(_pushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Общий звук уведомлений",
_pushSound,
(value) =>
_updateNotificationSetting(pushSound: value),
),
),
],
),
),
],
),
);
}
Widget _buildModalSettings(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Уведомления",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
_OutlinedSection(
child: Column(
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(
Icons.chat_bubble_outline,
),
title: const Text("Уведомления из чатов"),
value: _chatsPushNotification == 'ON',
onChanged: (value) =>
_updateNotificationSetting(
chatsPush: value,
),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(
Icons.phone_outlined,
),
title: const Text(
"Уведомления о звонках",
),
value: _mCallPushNotification,
onChanged: (value) =>
_updateNotificationSetting(
mCallPush: value,
),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(
Icons.visibility_outlined,
),
title: const Text("Показывать текст"),
subtitle: const Text(
"Показывать превью сообщения",
),
value: _pushDetails,
onChanged: (value) =>
_updateNotificationSetting(
pushDetails: value,
),
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(
Icons.music_note_outlined,
),
title: const Text("Звук в чатах"),
trailing: Text(
_getSoundDescription(_chatsPushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Звук уведомлений чатов",
_chatsPushSound,
(value) => _updateNotificationSetting(
chatsSound: value,
),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(
Icons.notifications_active_outlined,
),
title: const Text("Общий звук"),
trailing: Text(
_getSoundDescription(_pushSound),
style: TextStyle(color: colors.primary),
),
onTap: () => _showSoundDialog(
"Общий звук уведомлений",
_pushSound,
(value) => _updateNotificationSetting(
pushSound: value,
),
),
),
],
),
),
],
),
),
],
),
),
),
],
),
);
}
}
class _OutlinedSection extends StatelessWidget {
final Widget child;
const _OutlinedSection({required this.child});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: colors.outline.withOpacity(0.3)),
borderRadius: BorderRadius.circular(12),
),
child: child,
);
}
}

View File

@@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:gwid/screens/settings/privacy_settings_screen.dart';
import 'package:gwid/screens/settings/security_settings_screen.dart';
class PrivacySecurityScreen extends StatelessWidget {
final bool isModal;
const PrivacySecurityScreen({super.key, this.isModal = false});
@override
Widget build(BuildContext context) {
if (isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(title: const Text("Приватность и безопасность")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: const Text("Приватность"),
subtitle: const Text("Статус онлайн, кто может вас найти"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const PrivacySettingsScreen(),
),
);
},
),
),
const SizedBox(height: 10),
Card(
child: ListTile(
leading: const Icon(Icons.lock_outline),
title: const Text("Безопасность"),
subtitle: const Text("Пароль, сессии, заблокированные"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecuritySettingsScreen(),
),
);
},
),
),
],
),
);
}
Widget _buildModalSettings(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Приватность и безопасность",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: const Text("Приватность"),
subtitle: const Text("Статус онлайн, кто может вас найти"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const PrivacySettingsScreen(),
),
);
},
),
),
const SizedBox(height: 16),
Card(
child: ListTile(
leading: const Icon(Icons.security_outlined),
title: const Text("Безопасность"),
subtitle: const Text("Пароли, сессии, двухфакторная аутентификация"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecuritySettingsScreen(),
),
);
},
),
),
],
),
),
],
),
),
),
],
),
);
}
Widget buildModalContent(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: const Text("Приватность"),
subtitle: const Text("Статус онлайн, кто может вас найти"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const PrivacySettingsScreen(),
),
);
},
),
),
const SizedBox(height: 16),
Card(
child: ListTile(
leading: const Icon(Icons.security_outlined),
title: const Text("Безопасность"),
subtitle: const Text("Пароли, сессии, двухфакторная аутентификация"),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecuritySettingsScreen(),
),
);
},
),
),
],
);
}
}

View File

@@ -0,0 +1,383 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/theme_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/password_management_screen.dart';
class PrivacySettingsScreen extends StatefulWidget {
const PrivacySettingsScreen({super.key});
@override
State<PrivacySettingsScreen> createState() => _PrivacySettingsScreenState();
}
class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
bool _isHidden = false;
bool _isLoading = false;
String _searchByPhone = 'ALL'; // 'ALL', 'CONTACTS', 'NOBODY'
String _incomingCall = 'ALL'; // 'ALL', 'CONTACTS', 'NOBODY'
String _chatsInvite = 'ALL'; // 'ALL', 'CONTACTS', 'NOBODY'
@override
void initState() {
super.initState();
_loadCurrentSettings();
ApiService.instance.messages.listen((message) {
if (message['type'] == 'privacy_settings_updated' && mounted) {
_loadCurrentSettings();
}
});
}
Future<void> _loadCurrentSettings() async {
try {
final prefs = await SharedPreferences.getInstance();
setState(() {
_isHidden = prefs.getBool('privacy_hidden') ?? false;
_searchByPhone = prefs.getString('privacy_search_by_phone') ?? 'ALL';
_incomingCall = prefs.getString('privacy_incoming_call') ?? 'ALL';
_chatsInvite = prefs.getString('privacy_chats_invite') ?? 'ALL';
});
} catch (e) {
print('Ошибка загрузки настроек приватности: $e');
}
}
Future<void> _savePrivacySetting(String key, dynamic value) async {
try {
final prefs = await SharedPreferences.getInstance();
if (value is bool) {
await prefs.setBool(key, value);
} else if (value is String) {
await prefs.setString(key, value);
}
} catch (e) {
print('Ошибка сохранения настройки $key: $e');
}
}
Future<void> _updateHiddenStatus(bool hidden) async {
setState(() => _isLoading = true);
try {
await ApiService.instance.updatePrivacySettings(
hidden: hidden ? 'true' : 'false',
);
await _savePrivacySetting('privacy_hidden', hidden);
if (mounted) {
setState(() => _isHidden = hidden);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
hidden ? 'Статус онлайн скрыт' : 'Статус онлайн виден',
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
_showErrorSnackBar(e);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _updatePrivacyOption({
String? searchByPhone,
String? incomingCall,
String? chatsInvite,
}) async {
try {
await ApiService.instance.updatePrivacySettings(
searchByPhone: searchByPhone,
incomingCall: incomingCall,
chatsInvite: chatsInvite,
);
if (searchByPhone != null) {
await _savePrivacySetting('privacy_search_by_phone', searchByPhone);
if (mounted) setState(() => _searchByPhone = searchByPhone);
}
if (incomingCall != null) {
await _savePrivacySetting('privacy_incoming_call', incomingCall);
if (mounted) setState(() => _incomingCall = incomingCall);
}
if (chatsInvite != null) {
await _savePrivacySetting('privacy_chats_invite', chatsInvite);
if (mounted) setState(() => _chatsInvite = chatsInvite);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Настройки приватности обновлены'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
_showErrorSnackBar(e);
}
}
void _showErrorSnackBar(Object e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка обновления: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
void _showOptionDialog(
String title,
String currentValue,
Function(String) onSelect,
) {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(title),
children: [
_buildDialogOption(
'Все пользователи',
'ALL',
currentValue,
onSelect,
),
_buildDialogOption(
'Только контакты',
'CONTACTS',
currentValue,
onSelect,
),
_buildDialogOption('Никто', 'NOBODY', currentValue, onSelect),
],
);
},
);
}
Widget _buildDialogOption(
String title,
String value,
String groupValue,
Function(String) onSelect,
) {
return RadioListTile<String>(
title: Text(title),
value: value,
groupValue: groupValue,
onChanged: (newValue) {
if (newValue != null) {
onSelect(newValue);
Navigator.of(context).pop();
}
},
);
}
String _getPrivacyDescription(String value) {
switch (value) {
case 'ALL':
return 'Все пользователи';
case 'CONTACTS':
return 'Только контакты';
case 'NOBODY':
return 'Никто';
default:
return 'Неизвестно';
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final theme = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(title: const Text('Приватность')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Статус онлайн", colors),
if (_isLoading) const LinearProgressIndicator(),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: Icon(
_isHidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
title: const Text("Скрыть статус онлайн"),
subtitle: Text(
_isHidden
? "Другие не видят, что вы онлайн"
: "Другие видят ваш статус онлайн",
),
value: _isHidden,
onChanged: _isLoading ? null : _updateHiddenStatus,
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Взаимодействие", colors),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.search),
title: const Text("Кто может найти меня по номеру"),
subtitle: Text(_getPrivacyDescription(_searchByPhone)),
onTap: () => _showOptionDialog(
"Кто может найти вас?",
_searchByPhone,
(value) => _updatePrivacyOption(searchByPhone: value),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.phone_callback_outlined),
title: const Text("Кто может звонить мне"),
subtitle: Text(_getPrivacyDescription(_incomingCall)),
onTap: () => _showOptionDialog(
"Кто может вам звонить?",
_incomingCall,
(value) => _updatePrivacyOption(incomingCall: value),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.group_add_outlined),
title: const Text("Кто может приглашать в чаты"),
subtitle: Text(_getPrivacyDescription(_chatsInvite)),
onTap: () => _showOptionDialog(
"Кто может приглашать вас в чаты?",
_chatsInvite,
(value) => _updatePrivacyOption(chatsInvite: value),
),
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Пароль аккаунта", colors),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.lock_outline),
title: const Text("Установить пароль"),
subtitle: const Text(
"Добавить пароль для дополнительной защиты аккаунта",
),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const PasswordManagementScreen(),
),
);
},
),
],
),
),
const SizedBox(height: 16),
_OutlinedSection(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("Прочтение сообщений", colors),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.mark_chat_read_outlined),
title: const Text("Читать сообщения при входе"),
subtitle: const Text(
"Отмечать чат прочитанным при открытии",
),
value: theme.debugReadOnEnter,
onChanged: (value) => theme.setDebugReadOnEnter(value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.send_and_archive_outlined),
title: const Text("Читать при отправке сообщения"),
subtitle: const Text(
"Отмечать чат прочитанным при отправке сообщения",
),
value: theme.debugReadOnAction,
onChanged: (value) => theme.setDebugReadOnAction(value),
),
],
),
),
],
),
),
);
}
Widget _buildSectionTitle(String title, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: TextStyle(
color: colors.primary,
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
);
}
}
class _OutlinedSection extends StatelessWidget {
final Widget child;
const _OutlinedSection({required this.child});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: colors.outline.withOpacity(0.3)),
borderRadius: BorderRadius.circular(12),
),
child: child,
);
}
}

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/proxy_settings.dart';
class ProxySettingsScreen extends StatefulWidget {
const ProxySettingsScreen({super.key});
@override
State<ProxySettingsScreen> createState() => _ProxySettingsScreenState();
}
class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
final _formKey = GlobalKey<FormState>();
late ProxySettings _settings;
bool _isLoading = true;
bool _isTesting = false; // <-- НОВОЕ: Состояние для отслеживания проверки
final _hostController = TextEditingController();
final _portController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final settings = await ProxyService.instance.loadProxySettings();
setState(() {
_settings = settings;
_hostController.text = _settings.host;
_portController.text = _settings.port.toString();
_usernameController.text = _settings.username ?? '';
_passwordController.text = _settings.password ?? '';
_isLoading = false;
});
}
Future<void> _testProxyConnection() async {
if (_formKey.currentState?.validate() != true) {
return;
}
setState(() {
_isTesting = true;
});
final settingsToTest = ProxySettings(
isEnabled: true, // Для теста прокси всегда должен быть включен
protocol: _settings.protocol,
host: _hostController.text.trim(),
port: int.tryParse(_portController.text.trim()) ?? 8080,
username: _usernameController.text.trim(),
password: _passwordController.text.trim(),
);
try {
await ProxyService.instance.checkProxy(settingsToTest);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Прокси доступен и работает'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка подключения: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isTesting = false;
});
}
}
}
Future<void> _saveSettings() async {
if (_formKey.currentState!.validate()) {
final newSettings = ProxySettings(
isEnabled: _settings.isEnabled,
protocol: _settings.protocol,
host: _hostController.text.trim(),
port: int.tryParse(_portController.text.trim()) ?? 8080,
username: _usernameController.text.trim().isEmpty
? null
: _usernameController.text.trim(),
password: _passwordController.text.trim().isEmpty
? null
: _passwordController.text.trim(),
);
await ProxyService.instance.saveProxySettings(newSettings);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Настройки прокси сохранены. Перезайдите, чтобы применить.',
),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
}
}
@override
void dispose() {
_hostController.dispose();
_portController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Настройки прокси'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _isLoading || _isTesting ? null : _saveSettings,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Включить прокси'),
value: _settings.isEnabled,
onChanged: (value) {
setState(() {
_settings = _settings.copyWith(isEnabled: value);
});
},
),
const SizedBox(height: 16),
DropdownButtonFormField<ProxyProtocol>(
initialValue: _settings.protocol,
decoration: const InputDecoration(
labelText: 'Протокол',
border: OutlineInputBorder(),
),
items: ProxyProtocol.values
.where(
(p) =>
p != ProxyProtocol.socks4 &&
p != ProxyProtocol.socks5,
)
.map(
(protocol) => DropdownMenuItem(
value: protocol,
child: Text(protocol.name.toUpperCase()),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_settings = _settings.copyWith(protocol: value);
});
}
},
),
const SizedBox(height: 16),
TextFormField(
controller: _hostController,
decoration: const InputDecoration(
labelText: 'Хост',
border: OutlineInputBorder(),
),
validator: (value) {
if (_settings.isEnabled &&
(value == null || value.isEmpty)) {
return 'Укажите хост прокси-сервера';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _portController,
decoration: const InputDecoration(
labelText: 'Порт',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (_settings.isEnabled) {
if (value == null || value.isEmpty) {
return 'Укажите порт';
}
if (int.tryParse(value) == null) {
return 'Некорректный номер порта';
}
}
return null;
},
),
const SizedBox(height: 24),
Text(
'Аутентификация (необязательно)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Имя пользователя',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Пароль',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _isTesting ? null : _testProxyConnection,
icon: _isTesting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.shield_outlined),
label: Text(_isTesting ? 'Проверка...' : 'Проверить'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,301 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:gwid/api_service.dart';
import 'dart:convert';
class QrLoginScreen extends StatefulWidget {
const QrLoginScreen({super.key});
@override
State<QrLoginScreen> createState() => _QrLoginScreenState();
}
class _QrLoginScreenState extends State<QrLoginScreen> {
String? _token;
String? _qrData;
bool _isLoading = true;
bool _isQrVisible = false;
String? _error;
Timer? _qrRefreshTimer; // Таймер для регенерации QR-кода (1 раз в минуту)
Timer?
_countdownTimer; // 👈 1. Таймер для обратного отсчета (1 раз в секунду)
int _countdownSeconds = 60; // 👈 2. Переменная для хранения секунд
@override
void initState() {
super.initState();
_initializeAndStartTimers();
}
@override
void dispose() {
_qrRefreshTimer?.cancel();
_countdownTimer?.cancel(); // 👈 3. Не забываем отменить второй таймер
super.dispose();
}
Future<void> _initializeAndStartTimers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final token = ApiService.instance.token;
if (token == null || token.isEmpty) {
throw Exception("Не удалось получить токен авторизации.");
}
if (mounted) {
_token = token;
_regenerateQrData(); // Первичная генерация
_qrRefreshTimer?.cancel();
_qrRefreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
_regenerateQrData();
});
_startCountdownTimer();
setState(() => _isLoading = false);
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
}
void _regenerateQrData() {
if (_token == null) return;
final data = {
"type": "komet_auth_v1",
"token": _token!,
"timestamp": DateTime.now().millisecondsSinceEpoch,
};
if (mounted) {
setState(() {
_qrData = jsonEncode(data);
_countdownSeconds = 60; // Сбрасываем счетчик на 60
});
}
}
void _startCountdownTimer() {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
if (_countdownSeconds > 0) {
_countdownSeconds--;
} else {
_countdownSeconds = 60;
}
});
}
});
}
void _toggleQrVisibility() {
if (_token != null) {
setState(() {
_isQrVisible = !_isQrVisible;
});
}
}
Widget _buildContent() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 60),
const SizedBox(height: 16),
const Text(
"Ошибка загрузки данных",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: _initializeAndStartTimers,
icon: const Icon(Icons.refresh),
label: const Text("Повторить"),
),
],
),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildQrDisplay(),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: _toggleQrVisibility,
icon: Icon(
_isQrVisible
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
label: Text(
_isQrVisible ? "Скрыть QR-код" : "Показать QR-код для входа",
),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
Widget _buildQrDisplay() {
final colors = Theme.of(context).colorScheme;
if (!_isQrVisible) {
return Center(
child: Column(
children: [
Icon(
Icons.qr_code_scanner_rounded,
size: 150,
color: colors.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
"QR-код скрыт",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
"Нажмите кнопку ниже, чтобы отобразить его.",
style: TextStyle(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
);
}
return Center(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 2,
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: QrImageView(
data: _qrData!,
version: QrVersions.auto,
size: 280.0,
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.circle,
color: colors.primary,
),
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.circle,
color: colors.primary,
),
errorCorrectionLevel: QrErrorCorrectLevel.H,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timer_outlined,
color: colors.onSurfaceVariant,
size: 20,
),
const SizedBox(width: 8),
Text(
"Обновится через: $_countdownSeconds сек.",
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text("Вход по QR-коду")),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.gpp_bad_outlined, color: Colors.red, size: 32),
SizedBox(width: 16),
Expanded(
child: Text(
"Любой, кто отсканирует этот код, получит полный доступ к вашему аккаунту. Не показывайте его посторонним.",
style: TextStyle(fontWeight: FontWeight.w500),
),
),
],
),
),
const Spacer(),
_buildContent(),
const Spacer(flex: 2),
],
),
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class QrScannerScreen extends StatefulWidget {
const QrScannerScreen({super.key});
@override
State<QrScannerScreen> createState() => _QrScannerScreenState();
}
class _QrScannerScreenState extends State<QrScannerScreen> {
final MobileScannerController _scannerController = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
bool _isScanCompleted = false;
@override
void dispose() {
_scannerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Сканировать QR-код"),
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
body: Stack(
children: [
MobileScanner(
controller: _scannerController,
onDetect: (capture) {
if (!_isScanCompleted) {
final String? code = capture.barcodes.first.rawValue;
if (code != null) {
setState(() => _isScanCompleted = true);
Navigator.of(context).pop(code);
}
}
},
),
Center(
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,298 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/home_screen.dart';
class ReconnectionScreen extends StatefulWidget {
const ReconnectionScreen({super.key});
@override
State<ReconnectionScreen> createState() => _ReconnectionScreenState();
}
class _ReconnectionScreenState extends State<ReconnectionScreen> {
StreamSubscription? _apiSubscription;
String _statusMessage = 'Переподключение...';
bool _isReconnecting = true;
Timer? _timeoutTimer;
@override
void initState() {
super.initState();
_startFullReconnection();
_listenToApiMessages();
}
@override
void dispose() {
_apiSubscription?.cancel();
_timeoutTimer?.cancel();
super.dispose();
}
void _listenToApiMessages() {
_apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return;
print(
'ReconnectionScreen: Получено сообщение: opcode=${message['opcode']}, cmd=${message['cmd']}',
);
if (message['opcode'] == 19 && message['cmd'] == 1) {
final payload = message['payload'];
print('ReconnectionScreen: Получен opcode 19, payload: $payload');
if (payload != null && payload['token'] != null) {
print('ReconnectionScreen: Вызываем _onReconnectionSuccess()');
_onReconnectionSuccess();
return;
}
}
if (message['cmd'] == 3) {
final errorPayload = message['payload'];
String errorMessage = 'Ошибка переподключения';
if (errorPayload != null) {
if (errorPayload['localizedMessage'] != null) {
errorMessage = errorPayload['localizedMessage'];
} else if (errorPayload['message'] != null) {
errorMessage = errorPayload['message'];
}
}
_onReconnectionError(errorMessage);
}
});
}
void _onReconnectionSuccess() {
if (!mounted) return;
print('ReconnectionScreen: _onReconnectionSuccess() вызван');
_timeoutTimer?.cancel();
setState(() {
_statusMessage = 'Переподключение успешно!';
_isReconnecting = false;
});
print('ReconnectionScreen: Устанавливаем таймер для навигации...');
Timer(const Duration(milliseconds: 1500), () {
if (mounted) {
print('ReconnectionScreen: Навигация к HomeScreen...');
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const HomeScreen()),
(route) => false,
);
}
});
}
void _onReconnectionError(String error) {
if (!mounted) return;
_timeoutTimer?.cancel();
setState(() {
_statusMessage = 'Ошибка: $error';
_isReconnecting = false;
});
Timer(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_statusMessage = 'Нажмите для повторной попытки';
});
}
});
}
Future<void> _startFullReconnection() async {
try {
print('ReconnectionScreen: Начинаем полное переподключение...');
_timeoutTimer = Timer(const Duration(seconds: 30), () {
if (mounted && _isReconnecting) {
_onReconnectionError('Таймаут переподключения');
}
});
setState(() {
_statusMessage = 'Отключение от сервера...';
});
ApiService.instance.disconnect();
setState(() {
_statusMessage = 'Очистка кэшей...';
});
ApiService.instance.clearAllCaches();
setState(() {
_statusMessage = 'Подключение к серверу...';
});
await ApiService.instance.performFullReconnection();
setState(() {
_statusMessage = 'Аутентификация...';
});
final hasToken = await ApiService.instance.hasToken();
if (hasToken) {
setState(() {
_statusMessage = 'Аутентификация...';
});
await ApiService.instance.getChatsAndContacts();
setState(() {
_statusMessage = 'Загрузка данных...';
});
} else {
_onReconnectionError('Токен аутентификации не найден');
}
} catch (e) {
_onReconnectionError('Ошибка переподключения: ${e.toString()}');
}
}
void _retryReconnection() {
setState(() {
_statusMessage = 'Переподключение...';
_isReconnecting = true;
});
_startFullReconnection();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return WillPopScope(
onWillPop: () async => false, // Блокируем кнопку "Назад"
child: Scaffold(
backgroundColor: colors.surface,
body: Container(
width: double.infinity,
height: double.infinity,
color: colors.surface.withOpacity(0.95),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: colors.primaryContainer,
shape: BoxShape.circle,
),
child: _isReconnecting
? CircularProgressIndicator(
strokeWidth: 4,
valueColor: AlwaysStoppedAnimation<Color>(
colors.primary,
),
)
: Icon(
_statusMessage.contains('Ошибка')
? Icons.error_outline
: Icons.check_circle_outline,
size: 60,
color: _statusMessage.contains('Ошибка')
? colors.error
: colors.primary,
),
),
const SizedBox(height: 32),
Text(
'Переподключение',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_statusMessage,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 48),
if (!_isReconnecting && _statusMessage.contains('Нажмите'))
ElevatedButton.icon(
onPressed: _retryReconnection,
icon: const Icon(Icons.refresh),
label: const Text('Повторить'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 32),
Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: colors.primary, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
'Выполняется полное переподключение к серверу. Пожалуйста, подождите.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
import 'package:gwid/screens/settings/sessions_screen.dart';
import 'package:gwid/screens/settings/export_session_screen.dart';
import 'package:gwid/screens/settings/qr_login_screen.dart';
class SecuritySettingsScreen extends StatelessWidget {
const SecuritySettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Безопасность")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSecurityOption(
context,
icon: Icons.qr_code_scanner_outlined,
title: "Вход по QR-коду",
subtitle: "Показать QR-код для входа на другом устройстве",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const QrLoginScreen()),
);
},
),
_buildSecurityOption(
context,
icon: Icons.history_toggle_off,
title: "Активные сессии",
subtitle: "Просмотр и управление активными сессиями",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SessionsScreen()),
);
},
),
_buildSecurityOption(
context,
icon: Icons.upload_file_outlined,
title: "Экспорт сессии",
subtitle: "Сохранить данные сессии для переноса",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ExportSessionScreen(),
),
);
},
),
_buildSecurityOption(
context,
icon: Icons.devices_other_outlined,
title: "Подмена данных сессии",
subtitle: "Изменение User-Agent, версии ОС и т.д.",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SessionSpoofingScreen(),
),
);
},
),
],
),
);
}
Widget _buildSecurityOption(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: onTap,
),
);
}
}

View File

@@ -0,0 +1,754 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:gwid/services/version_checker.dart';
import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/api_service.dart';
import 'package:uuid/uuid.dart';
import 'package:gwid/device_presets.dart';
import 'package:gwid/phone_entry_screen.dart';
enum SpoofingMethod { partial, full }
enum PresetCategory { web, device }
class SessionSpoofingScreen extends StatefulWidget {
const SessionSpoofingScreen({super.key});
@override
State<SessionSpoofingScreen> createState() => _SessionSpoofingScreenState();
}
class _SessionSpoofingScreenState extends State<SessionSpoofingScreen> {
final _random = Random();
final _uuid = const Uuid();
final _userAgentController = TextEditingController();
final _deviceNameController = TextEditingController();
final _osVersionController = TextEditingController();
final _screenController = TextEditingController();
final _timezoneController = TextEditingController();
final _localeController = TextEditingController();
final _deviceIdController = TextEditingController();
final _appVersionController = TextEditingController();
String _selectedDeviceType = 'WEB';
SpoofingMethod _selectedMethod = SpoofingMethod.partial;
PresetCategory _selectedCategory = PresetCategory.web;
bool _isCheckingVersion = false;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadInitialData();
}
Future<void> _loadInitialData() async {
setState(() => _isLoading = true);
final prefs = await SharedPreferences.getInstance();
final isSpoofingEnabled = prefs.getBool('spoofing_enabled') ?? false;
if (isSpoofingEnabled) {
_userAgentController.text = prefs.getString('spoof_useragent') ?? '';
_deviceNameController.text = prefs.getString('spoof_devicename') ?? '';
_osVersionController.text = prefs.getString('spoof_osversion') ?? '';
_screenController.text = prefs.getString('spoof_screen') ?? '';
_timezoneController.text = prefs.getString('spoof_timezone') ?? '';
_localeController.text = prefs.getString('spoof_locale') ?? '';
_deviceIdController.text = prefs.getString('spoof_deviceid') ?? '';
_appVersionController.text =
prefs.getString('spoof_appversion') ?? '25.10.10';
_selectedDeviceType = prefs.getString('spoof_devicetype') ?? 'WEB';
if (_selectedDeviceType == 'WEB') {
_selectedCategory = PresetCategory.web;
} else {
_selectedCategory = PresetCategory.device;
}
} else {
setState(() {
_selectedCategory = PresetCategory.web;
});
await _applyGeneratedData();
}
setState(() => _isLoading = false);
}
Future<void> _loadDeviceData() async {
setState(() => _isLoading = true);
final deviceInfo = DeviceInfoPlugin();
final pixelRatio = View.of(context).devicePixelRatio;
final size = View.of(context).physicalSize;
_appVersionController.text = '25.10.10';
_localeController.text = Platform.localeName.split('_').first;
_screenController.text =
'${size.width.round()}x${size.height.round()} ${pixelRatio.toStringAsFixed(1)}x';
_deviceIdController.text = _uuid.v4();
try {
final timezoneInfo = await FlutterTimezone.getLocalTimezone();
_timezoneController.text = timezoneInfo.identifier;
} catch (e) {
_timezoneController.text = 'Europe/Moscow';
}
if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
_deviceNameController.text =
'${androidInfo.manufacturer} ${androidInfo.model}';
_osVersionController.text = 'Android ${androidInfo.version.release}';
_userAgentController.text =
'Mozilla/5.0 (Linux; Android ${androidInfo.version.release}; ${androidInfo.model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36';
_selectedDeviceType = 'ANDROID';
} else if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
_deviceNameController.text = iosInfo.name;
_osVersionController.text =
'${iosInfo.systemName} ${iosInfo.systemVersion}';
_userAgentController.text =
'Mozilla/5.0 (iPhone; CPU iPhone OS ${iosInfo.systemVersion.replaceAll('.', '_')} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
_selectedDeviceType = 'IOS';
} else {
setState(() => _selectedCategory = PresetCategory.device);
await _applyGeneratedData();
}
setState(() {
_selectedCategory = PresetCategory.device;
_isLoading = false;
});
}
Future<void> _applyGeneratedData() async {
final List<DevicePreset> filteredPresets;
if (_selectedCategory == PresetCategory.web) {
filteredPresets = devicePresets
.where((p) => p.deviceType == 'WEB')
.toList();
} else {
filteredPresets = devicePresets
.where((p) => p.deviceType != 'WEB')
.toList();
}
if (filteredPresets.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Нет доступных пресетов для этой категории.'),
),
);
}
return;
}
final preset = filteredPresets[_random.nextInt(filteredPresets.length)];
await _applyPreset(preset);
}
Future<void> _applyPreset(DevicePreset preset) async {
setState(() {
_userAgentController.text = preset.userAgent;
_deviceNameController.text = preset.deviceName;
_osVersionController.text = preset.osVersion;
_screenController.text = preset.screen;
_appVersionController.text = '25.10.10';
_deviceIdController.text = _uuid.v4();
if (_selectedMethod == SpoofingMethod.partial) {
_selectedDeviceType = 'WEB';
} else {
_selectedDeviceType = preset.deviceType;
_timezoneController.text = preset.timezone;
_localeController.text = preset.locale;
}
});
if (_selectedMethod == SpoofingMethod.partial) {
String timezone;
try {
final timezoneInfo = await FlutterTimezone.getLocalTimezone();
timezone = timezoneInfo.identifier;
} catch (_) {
timezone = 'Europe/Moscow';
}
final locale = Platform.localeName.split('_').first;
if (mounted) {
setState(() {
_timezoneController.text = timezone;
_localeController.text = locale;
});
}
}
}
Future<void> _saveSpoofingSettings() async {
if (!mounted) return;
final prefs = await SharedPreferences.getInstance();
final oldValues = {
'user_agent': prefs.getString('spoof_useragent') ?? '',
'device_name': prefs.getString('spoof_devicename') ?? '',
'os_version': prefs.getString('spoof_osversion') ?? '',
'screen': prefs.getString('spoof_screen') ?? '',
'timezone': prefs.getString('spoof_timezone') ?? '',
'locale': prefs.getString('spoof_locale') ?? '',
'device_id': prefs.getString('spoof_deviceid') ?? '',
'device_type': prefs.getString('spoof_devicetype') ?? 'WEB',
};
final newValues = {
'user_agent': _userAgentController.text,
'device_name': _deviceNameController.text,
'os_version': _osVersionController.text,
'screen': _screenController.text,
'timezone': _timezoneController.text,
'locale': _localeController.text,
'device_id': _deviceIdController.text,
'device_type': _selectedDeviceType,
};
final oldAppVersion = prefs.getString('spoof_appversion') ?? '25.10.10';
final newAppVersion = _appVersionController.text;
bool otherDataChanged = false;
for (final key in oldValues.keys) {
if (oldValues[key] != newValues[key]) {
otherDataChanged = true;
break;
}
}
final appVersionChanged = oldAppVersion != newAppVersion;
if (appVersionChanged && !otherDataChanged) {
await _saveAllData(prefs);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Настройки применятся при следующем входе в приложение.',
),
duration: Duration(seconds: 3),
),
);
Navigator.of(context).pop();
}
}
else {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Применить настройки?'),
content: const Text(
'Для применения настроек потребуется перезайти в аккаунт. Вы уверены?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Применить'),
),
],
),
);
if (confirmed != true || !mounted) return;
await _saveAllData(prefs);
try {
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const PhoneEntryScreen()),
(route) => false,
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при выходе: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
}
}
Future<void> _saveAllData(SharedPreferences prefs) async {
await prefs.setBool('spoofing_enabled', true);
await prefs.setString('spoof_useragent', _userAgentController.text);
await prefs.setString('spoof_devicename', _deviceNameController.text);
await prefs.setString('spoof_osversion', _osVersionController.text);
await prefs.setString('spoof_screen', _screenController.text);
await prefs.setString('spoof_timezone', _timezoneController.text);
await prefs.setString('spoof_locale', _localeController.text);
await prefs.setString('spoof_deviceid', _deviceIdController.text);
await prefs.setString('spoof_devicetype', _selectedDeviceType);
await prefs.setString('spoof_appversion', _appVersionController.text);
}
Future<void> _handleVersionCheck() async {
if (_isCheckingVersion) return;
setState(() => _isCheckingVersion = true);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Проверяю последнюю версию...')),
);
try {
final latestVersion = await VersionChecker.getLatestVersion();
if (mounted) {
setState(() => _appVersionController.text = latestVersion);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Найдена версия: $latestVersion'),
backgroundColor: Colors.green.shade700,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()), // Показываем ошибку из VersionChecker
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
} finally {
if (mounted) {
setState(() => _isCheckingVersion = false);
}
}
}
@override
void dispose() {
_userAgentController.dispose();
_deviceNameController.dispose();
_osVersionController.dispose();
_screenController.dispose();
_timezoneController.dispose();
_localeController.dispose();
_deviceIdController.dispose();
_appVersionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Подмена данных сессии'),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildInfoCard(),
const SizedBox(height: 16),
_buildSpoofingMethodCard(),
const SizedBox(height: 16),
_buildPresetTypeCard(),
const SizedBox(height: 24),
_buildMainDataCard(),
const SizedBox(height: 16),
_buildRegionalDataCard(),
const SizedBox(height: 16),
_buildIdentifiersCard(),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _buildFloatingActionButtons(),
);
}
Widget _buildInfoCard() {
return Card(
color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.5),
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text.rich(
TextSpan(
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
children: const [
TextSpan(
text: 'Нажмите ',
style: TextStyle(fontWeight: FontWeight.w500),
),
WidgetSpan(
child: Icon(Icons.touch_app, size: 16),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: ' "Сгенерировать":\n'),
TextSpan(
text: '• Короткое нажатие: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: 'случайный пресет.\n'),
TextSpan(
text: '• Длинное нажатие: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: 'реальные данные.'),
],
),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildSpoofingMethodCard() {
final theme = Theme.of(context);
Widget descriptionWidget;
if (_selectedMethod == SpoofingMethod.partial) {
descriptionWidget = _buildDescriptionTile(
icon: Icons.check_circle_outline,
color: Colors.green.shade700,
text:
'Рекомендуемый метод. Используются случайные данные, но ваш реальный часовой пояс и локаль для большей правдоподобности.',
);
} else {
descriptionWidget = _buildDescriptionTile(
icon: Icons.warning_amber_rounded,
color: theme.colorScheme.error,
text:
'Все данные, включая часовой пояс и локаль, генерируются случайно. Использование этого метода на ваш страх и риск!',
);
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text("Метод подмены", style: theme.textTheme.titleMedium),
const SizedBox(height: 12),
SegmentedButton<SpoofingMethod>(
style: SegmentedButton.styleFrom(shape: const StadiumBorder()),
segments: const [
ButtonSegment(
value: SpoofingMethod.partial,
label: Text('Частичный'),
icon: Icon(Icons.security_outlined),
),
ButtonSegment(
value: SpoofingMethod.full,
label: Text('Полный'),
icon: Icon(Icons.public_outlined),
),
],
selected: {_selectedMethod},
onSelectionChanged: (s) =>
setState(() => _selectedMethod = s.first),
),
const SizedBox(height: 12),
descriptionWidget,
],
),
),
);
}
Widget _buildDescriptionTile({
required IconData icon,
required Color color,
required String text,
}) {
return ListTile(
leading: Icon(icon, color: color),
contentPadding: EdgeInsets.zero,
title: Text(
text,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}
Widget _buildPresetTypeCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
"Тип пресетов для генерации",
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
SegmentedButton<PresetCategory>(
style: SegmentedButton.styleFrom(shape: const StadiumBorder()),
segments: const <ButtonSegment<PresetCategory>>[
ButtonSegment(
value: PresetCategory.web,
label: Text('Веб'),
icon: Icon(Icons.web_outlined),
),
ButtonSegment(
value: PresetCategory.device,
label: Text('Устройства'),
icon: Icon(Icons.devices_outlined),
),
],
selected: {_selectedCategory},
onSelectionChanged: (newSelection) {
setState(() => _selectedCategory = newSelection.first);
_applyGeneratedData();
},
),
],
),
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildMainDataCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, "Основные данные"),
TextField(
controller: _userAgentController,
decoration: _inputDecoration('User-Agent', Icons.http_outlined),
maxLines: 3,
minLines: 2,
),
const SizedBox(height: 16),
TextField(
controller: _deviceNameController,
decoration: _inputDecoration(
'Имя устройства',
Icons.smartphone_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _osVersionController,
decoration: _inputDecoration('Версия ОС', Icons.layers_outlined),
),
],
),
),
);
}
Widget _buildRegionalDataCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, "Региональные данные"),
TextField(
controller: _screenController,
decoration: _inputDecoration(
'Разрешение экрана',
Icons.fullscreen_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _timezoneController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
'Часовой пояс',
Icons.public_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _localeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration('Локаль', Icons.language_outlined),
),
],
),
),
);
}
Widget _buildIdentifiersCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, "Идентификаторы"),
TextField(
controller: _deviceIdController,
decoration: _inputDecoration('ID Устройства', Icons.tag_outlined),
),
const SizedBox(height: 16),
TextField(
controller: _appVersionController,
decoration:
_inputDecoration(
'Версия приложения',
Icons.info_outline_rounded,
).copyWith(
suffixIcon: _isCheckingVersion
? const Padding(
padding: EdgeInsets.all(12.0),
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
),
),
)
: IconButton(
icon: const Icon(Icons.cloud_sync_outlined),
tooltip: 'Проверить последнюю версию',
onPressed:
_handleVersionCheck, // Вот здесь вызывается ваша функция
),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedDeviceType,
decoration: _inputDecoration(
'Тип устройства',
Icons.devices_other_outlined,
),
items: const [
DropdownMenuItem(value: 'ANDROID', child: Text('ANDROID')),
DropdownMenuItem(value: 'IOS', child: Text('IOS')),
DropdownMenuItem(value: 'DESKTOP', child: Text('DESKTOP')),
DropdownMenuItem(value: 'WEB', child: Text('WEB')),
],
onChanged: (v) =>
v != null ? setState(() => _selectedDeviceType = v) : null,
),
],
),
),
);
}
InputDecoration _inputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
);
}
Widget _buildFloatingActionButtons() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
flex: 3,
child: FilledButton.tonal(
onPressed: _applyGeneratedData,
onLongPress: _loadDeviceData,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
shape: const StadiumBorder(),
),
child: const Text('Сгенерировать'),
),
),
const SizedBox(width: 12),
Expanded(
flex: 4,
child: FilledButton(
onPressed: _saveSpoofingSettings,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
shape: const StadiumBorder(),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.save_alt_outlined),
SizedBox(width: 8),
Text('Применить'),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'dart:async';
import 'package:intl/intl.dart';
class Session {
final String client;
final String location;
final bool current;
final int time;
final String info;
Session({
required this.client,
required this.location,
required this.current,
required this.time,
required this.info,
});
factory Session.fromJson(Map<String, dynamic> json) {
return Session(
client: json['client'] ?? 'Неизвестное устройство',
location: json['location'] ?? 'Неизвестное местоположение',
current: json['current'] ?? false,
time: json['time'] ?? 0,
info: json['info'] ?? 'Нет дополнительной информации',
);
}
}
class SessionsScreen extends StatefulWidget {
const SessionsScreen({super.key});
@override
State<SessionsScreen> createState() => _SessionsScreenState();
}
class _SessionsScreenState extends State<SessionsScreen> {
List<Session> _sessions = [];
bool _isLoading = true;
StreamSubscription? _apiSubscription;
@override
void initState() {
super.initState();
_listenToApi();
_loadSessions();
}
void _listenToApi() {
_apiSubscription = ApiService.instance.messages.listen((message) {
if (message['opcode'] == 96 && mounted) {
final payload = message['payload'];
if (payload != null && payload['sessions'] != null) {
final sessionsList = payload['sessions'] as List;
setState(() {
_sessions = sessionsList
.map((session) => Session.fromJson(session))
.toList();
_isLoading = false;
});
}
}
});
}
void _loadSessions() {
setState(() => _isLoading = true);
ApiService.instance.requestSessions();
}
void _terminateAllSessions() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Завершить другие сессии?'),
content: const Text(
'Все сессии, кроме текущей, будут завершены. Вы уверены?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Завершить'),
),
],
),
);
if (confirmed == true) {
ApiService.instance.terminateAllSessions();
Future.delayed(const Duration(seconds: 1), _loadSessions);
}
}
String _formatTime(int timestamp) {
final date = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 0) {
return '${difference.inDays} д. назад';
} else if (difference.inHours > 0) {
return '${difference.inHours} ч. назад';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes} м. назад';
} else {
return 'Только что';
}
}
IconData _getDeviceIcon(String clientInfo) {
final lowerInfo = clientInfo.toLowerCase();
if (lowerInfo.contains('windows') ||
lowerInfo.contains('linux') ||
lowerInfo.contains('macos')) {
return Icons.computer_outlined;
} else if (lowerInfo.contains('iphone') || lowerInfo.contains('ios')) {
return Icons.phone_iphone;
} else if (lowerInfo.contains('android')) {
return Icons.phone_android;
}
return Icons.web_asset;
}
@override
void dispose() {
_apiSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text("Активные сессии"),
actions: [
IconButton(onPressed: _loadSessions, icon: const Icon(Icons.refresh)),
],
),
body: Column(
children: [
if (_sessions.any((s) => !s.current))
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _terminateAllSessions,
icon: const Icon(Icons.logout),
label: const Text("Завершить другие сессии"),
style: FilledButton.styleFrom(
backgroundColor: colors.errorContainer,
foregroundColor: colors.onErrorContainer,
),
),
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _sessions.isEmpty
? Center(
child: Text(
"Активных сессий не найдено.",
style: TextStyle(color: colors.onSurfaceVariant),
),
)
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _sessions.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
final session = _sessions[index];
final deviceIcon = _getDeviceIcon(session.client);
return Card(
elevation: session.current ? 4 : 1,
shape: RoundedRectangleBorder(
side: BorderSide(
color: session.current
? colors.primary
: Colors.transparent,
width: 1.5,
),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(deviceIcon, size: 40, color: colors.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
session.client,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
session.location,
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
"Последняя активность: ${_formatTime(session.time)}",
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 12,
),
),
],
),
),
if (session.current)
const Icon(
Icons.check_circle,
color: Colors.green,
size: 24,
),
],
),
),
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,610 @@
import 'package:flutter/material.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/manage_account_screen.dart';
import 'package:gwid/screens/settings/appearance_settings_screen.dart';
import 'package:gwid/screens/settings/notification_settings_screen.dart';
import 'package:gwid/screens/settings/privacy_security_screen.dart';
import 'package:gwid/screens/settings/storage_screen.dart';
import 'package:gwid/screens/settings/network_settings_screen.dart';
import 'package:gwid/screens/settings/bypass_screen.dart';
import 'package:gwid/screens/settings/about_screen.dart';
import 'package:gwid/debug_screen.dart';
import 'package:gwid/screens/settings/komet_misc_screen.dart';
import 'package:gwid/theme_provider.dart';
import 'package:provider/provider.dart';
class SettingsScreen extends StatefulWidget {
final bool showBackToChats;
final VoidCallback? onBackToChats;
final Profile? myProfile;
final bool isModal;
const SettingsScreen({
super.key,
this.showBackToChats = false,
this.onBackToChats,
this.myProfile,
this.isModal = false,
});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
Profile? _myProfile;
bool _isProfileLoading = true;
int _versionTapCount = 0;
DateTime? _lastTapTime;
bool _isReconnecting = false;
String _currentModalScreen = 'main';
@override
void initState() {
super.initState();
if (widget.myProfile != null) {
_myProfile = widget.myProfile;
_isProfileLoading = false;
} else {
_loadMyProfile();
}
}
Future<void> _loadMyProfile() async {
if (!mounted) return;
setState(() => _isProfileLoading = true);
final cachedProfileData = ApiService.instance.lastChatsPayload?['profile'];
if (cachedProfileData != null && mounted) {
setState(() {
_myProfile = Profile.fromJson(cachedProfileData);
_isProfileLoading = false;
});
return; // Нашли в кеше, выходим
}
try {
final result = await ApiService.instance.getChatsAndContacts(force: true);
if (mounted) {
final profileJson = result['profile'];
if (profileJson != null) {
setState(() {
_myProfile = Profile.fromJson(profileJson);
_isProfileLoading = false;
});
} else {
setState(() => _isProfileLoading = false);
}
}
} catch (e) {
if (mounted) {
setState(() => _isProfileLoading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Ошибка загрузки профиля: $e")));
}
}
}
Future<void> _handleReconnection() async {
if (_isReconnecting) return;
setState(() {
_isReconnecting = true;
});
try {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Переподключение...'),
duration: Duration(seconds: 2),
),
);
}
await ApiService.instance.performFullReconnection();
await _loadMyProfile();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Переподключение выполнено успешно'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка переподключения: $e'),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
}
} finally {
if (mounted) {
setState(() {
_isReconnecting = false;
});
}
}
}
Widget _buildReconnectionButton() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Icon(
Icons.sync,
color: _isReconnecting
? Colors.grey
: Theme.of(context).colorScheme.primary,
),
title: const Text(
"Переподключиться к серверу",
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: const Text("Сбросить соединение и переподключиться"),
trailing: _isReconnecting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).colorScheme.primary,
),
onTap: _isReconnecting ? null : _handleReconnection,
),
);
}
void _handleVersionTap() {
final now = DateTime.now();
if (_lastTapTime != null && now.difference(_lastTapTime!).inSeconds > 2) {
_versionTapCount = 0;
}
_lastTapTime = now;
_versionTapCount++;
if (_versionTapCount >= 7) {
_versionTapCount = 0;
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const DebugScreen()));
}
}
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final isDesktop = themeProvider.useDesktopLayout;
if (widget.isModal || isDesktop) {
return _buildModalSettings(context);
}
return Scaffold(
appBar: AppBar(
title: const Text("Настройки"),
leading: widget.showBackToChats
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: widget.onBackToChats,
)
: null,
),
body: _buildSettingsContent(),
);
}
Widget _buildModalSettings(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final screenSize = MediaQuery.of(context).size;
final screenWidth = screenSize.width;
final screenHeight = screenSize.height;
final isSmallScreen = screenWidth < 600 || screenHeight < 800;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withValues(alpha: 0.3),
),
),
Center(
child: isSmallScreen
? Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(color: colors.surface),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: colors.surface),
child: Row(
children: [
if (_currentModalScreen != 'main')
IconButton(
onPressed: () {
setState(() {
_currentModalScreen = 'main';
});
},
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
Expanded(
child: Text(
_getModalTitle(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(child: _buildModalContent()),
],
),
)
: Container(
width: 400,
height: 900,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
if (_currentModalScreen != 'main')
IconButton(
onPressed: () {
setState(() {
_currentModalScreen = 'main';
});
},
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
Expanded(
child: Text(
_getModalTitle(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(child: _buildModalContent()),
],
),
),
),
],
),
);
}
String _getModalTitle() {
switch (_currentModalScreen) {
case 'notifications':
return 'Уведомления';
case 'appearance':
return 'Внешний вид';
case 'privacy':
return 'Приватность и безопасность';
case 'storage':
return 'Хранилище';
case 'network':
return 'Сеть';
case 'bypass':
return 'Bypass';
case 'about':
return 'О приложении';
case 'komet':
return 'Komet Misc';
default:
return 'Настройки';
}
}
Widget _buildModalContent() {
switch (_currentModalScreen) {
case 'notifications':
return const NotificationSettingsScreen(isModal: true);
case 'appearance':
return const AppearanceSettingsScreen(isModal: true);
case 'privacy':
return const PrivacySecurityScreen(isModal: true);
case 'storage':
return const StorageScreen(isModal: true);
case 'network':
return const NetworkSettingsScreen(isModal: true);
case 'bypass':
return const BypassScreen(isModal: true);
case 'about':
return const AboutScreen(isModal: true);
case 'komet':
return const KometMiscScreen(isModal: true);
default:
return _buildSettingsContent();
}
}
Widget _buildSettingsContent() {
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
_buildProfileSection(),
const SizedBox(height: 16),
_buildReconnectionButton(),
const SizedBox(height: 16),
_buildSettingsCategory(
context,
icon: Icons.rocket_launch_outlined,
title: "Komet Misc",
subtitle: "Дополнительные настройки",
screen: KometMiscScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.palette_outlined,
title: "Внешний вид",
subtitle: "Темы, анимации, производительность",
screen: AppearanceSettingsScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.notifications_outlined,
title: "Уведомления",
subtitle: "Звуки, чаты, звонки",
screen: NotificationSettingsScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.security_outlined,
title: "Приватность и безопасность",
subtitle: "Статус, сессии, пароль, блокировки",
screen: PrivacySecurityScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.storage_outlined,
title: "Данные и хранилище",
subtitle: "Использование хранилища, очистка кэша",
screen: StorageScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.wifi_outlined,
title: "Сеть",
subtitle: "Прокси, мониторинг, логи",
screen: NetworkSettingsScreen(isModal: widget.isModal),
),
_buildSettingsCategory(
context,
icon: Icons.psychology_outlined,
title: "Специальные возможности",
subtitle: "Обход ограничений",
screen: const BypassScreen(),
),
_buildSettingsCategory(
context,
icon: Icons.info_outline,
title: "О приложении",
subtitle: "Команда, соглашение",
screen: const AboutScreen(),
),
GestureDetector(
onTap: _handleVersionTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
'v0.3.0-beta.1',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.4),
fontSize: 12,
),
),
),
),
],
);
}
Widget _buildProfileSection() {
if (_isProfileLoading) {
return const Card(
child: ListTile(
leading: CircleAvatar(radius: 28),
title: Text("Загрузка профиля..."),
subtitle: Text("Пожалуйста, подождите"),
),
);
}
if (_myProfile == null) {
return Card(
child: ListTile(
leading: const CircleAvatar(
radius: 28,
child: Icon(Icons.error_outline),
),
title: const Text("Не удалось загрузить профиль"),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadMyProfile,
),
),
);
}
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: CircleAvatar(
radius: 28,
backgroundImage: _myProfile!.photoBaseUrl != null
? NetworkImage(_myProfile!.photoBaseUrl!)
: null,
child: _myProfile!.photoBaseUrl == null
? Text(
_myProfile!.displayName.isNotEmpty
? _myProfile!.displayName[0].toUpperCase()
: '',
style: const TextStyle(fontSize: 24),
)
: null,
),
title: Text(
_myProfile!.displayName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_myProfile!.formattedPhone),
const SizedBox(height: 2),
Text(
'ID: ${_myProfile!.id}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ManageAccountScreen(myProfile: _myProfile!),
),
);
},
),
);
}
Widget _buildSettingsCategory(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required Widget screen,
}) {
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () {
if (widget.isModal) {
String screenKey = '';
if (screen is NotificationSettingsScreen)
screenKey = 'notifications';
else if (screen is AppearanceSettingsScreen)
screenKey = 'appearance';
else if (screen is PrivacySecurityScreen)
screenKey = 'privacy';
else if (screen is StorageScreen)
screenKey = 'storage';
else if (screen is NetworkSettingsScreen)
screenKey = 'network';
else if (screen is BypassScreen)
screenKey = 'bypass';
else if (screen is AboutScreen)
screenKey = 'about';
else if (screen is KometMiscScreen)
screenKey = 'komet';
setState(() {
_currentModalScreen = screenKey;
});
} else {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => screen));
}
},
),
);
}
}

View File

@@ -0,0 +1,513 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
enum LogType { send, receive, status, pingpong }
class LogEntry {
final DateTime timestamp;
final String message;
final int id;
final LogType type;
LogEntry({
required this.timestamp,
required this.message,
required this.id,
required this.type,
});
}
class SocketLogScreen extends StatefulWidget {
const SocketLogScreen({super.key});
@override
State<SocketLogScreen> createState() => _SocketLogScreenState();
}
class _SocketLogScreenState extends State<SocketLogScreen> {
final List<LogEntry> _allLogEntries = [];
List<LogEntry> _filteredLogEntries = [];
StreamSubscription? _logSubscription;
final ScrollController _scrollController = ScrollController();
int _logIdCounter = 0;
bool _isAutoScrollEnabled = true;
bool _isSearchActive = false;
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
final Set<LogType> _activeFilters = {
LogType.send,
LogType.receive,
LogType.status,
LogType.pingpong,
};
@override
void initState() {
super.initState();
_searchController.addListener(() {
if (_searchQuery != _searchController.text) {
setState(() {
_searchQuery = _searchController.text;
_applyFiltersAndSearch();
});
}
});
_loadInitialLogs();
_subscribeToNewLogs();
}
LogType _getLogType(String message) {
if (message.contains('(ping)') || message.contains('(pong)')) {
return LogType.pingpong;
}
if (message.startsWith('➡️ SEND')) return LogType.send;
if (message.startsWith('⬅️ RECV')) return LogType.receive;
return LogType.status;
}
void _addLogEntry(String logMessage, {bool isInitial = false}) {
final newEntry = LogEntry(
id: _logIdCounter++,
timestamp: DateTime.now(),
message: logMessage,
type: _getLogType(logMessage),
);
_allLogEntries.add(newEntry);
if (!isInitial) {
_applyFiltersAndSearch();
if (_isAutoScrollEnabled) _scrollToBottom();
}
}
void _loadInitialLogs() {
final cachedLogs = ApiService.instance.connectionLogCache;
for (var log in cachedLogs) {
_addLogEntry(log, isInitial: true);
}
_applyFiltersAndSearch();
setState(
() {},
); // Однократное обновление UI после загрузки всех кэшированных логов
}
void _subscribeToNewLogs() {
_logSubscription = ApiService.instance.connectionLog.listen((logMessage) {
if (mounted) {
if (_allLogEntries.isNotEmpty &&
_allLogEntries.last.message == logMessage) {
return;
}
setState(() => _addLogEntry(logMessage));
}
});
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _applyFiltersAndSearch() {
List<LogEntry> tempFiltered = _allLogEntries.where((entry) {
return _activeFilters.contains(entry.type);
}).toList();
if (_searchQuery.isNotEmpty) {
tempFiltered = tempFiltered.where((entry) {
return entry.message.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
}
setState(() {
_filteredLogEntries = tempFiltered;
});
}
void _copyLogsToClipboard() {
final logText = _filteredLogEntries
.map(
(entry) =>
"[${DateFormat('HH:mm:ss.SSS').format(entry.timestamp)}] ${entry.message}",
)
.join('\n\n');
Clipboard.setData(ClipboardData(text: logText));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Отфильтрованный журнал скопирован')),
);
}
void _shareLogs() async {
final logText = _filteredLogEntries
.map(
(entry) =>
"[${DateFormat('HH:mm:ss.SSS').format(entry.timestamp)}] ${entry.message}",
)
.join('\n\n');
await Share.share(logText, subject: 'Gwid Connection Log');
}
void _clearLogs() {
setState(() {
_allLogEntries.clear();
_filteredLogEntries.clear();
});
}
void _showFilterDialog() {
showModalBottomSheet(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setSheetState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Фильтры логов",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Исходящие (SEND)'),
value: _activeFilters.contains(LogType.send),
onChanged: (val) {
setSheetState(
() => val
? _activeFilters.add(LogType.send)
: _activeFilters.remove(LogType.send),
);
_applyFiltersAndSearch();
},
),
SwitchListTile(
title: const Text('Входящие (RECV)'),
value: _activeFilters.contains(LogType.receive),
onChanged: (val) {
setSheetState(
() => val
? _activeFilters.add(LogType.receive)
: _activeFilters.remove(LogType.receive),
);
_applyFiltersAndSearch();
},
),
SwitchListTile(
title: const Text('Статус подключения'),
value: _activeFilters.contains(LogType.status),
onChanged: (val) {
setSheetState(
() => val
? _activeFilters.add(LogType.status)
: _activeFilters.remove(LogType.status),
);
_applyFiltersAndSearch();
},
),
SwitchListTile(
title: const Text('Ping/Pong'),
value: _activeFilters.contains(LogType.pingpong),
onChanged: (val) {
setSheetState(
() => val
? _activeFilters.add(LogType.pingpong)
: _activeFilters.remove(LogType.pingpong),
);
_applyFiltersAndSearch();
},
),
],
),
);
},
);
},
);
}
@override
void dispose() {
_logSubscription?.cancel();
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
AppBar _buildDefaultAppBar() {
return AppBar(
title: const Text("Журнал подключения"),
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: "Поиск",
onPressed: () => setState(() => _isSearchActive = true),
),
IconButton(
icon: Icon(
_activeFilters.length == 4
? Icons.filter_list
: Icons.filter_list_off,
),
tooltip: "Фильтры",
onPressed: _showFilterDialog,
),
IconButton(
icon: const Icon(Icons.delete_sweep),
tooltip: "Очистить",
onPressed: _allLogEntries.isNotEmpty ? _clearLogs : null,
),
],
);
}
AppBar _buildSearchAppBar() {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
_isSearchActive = false;
_searchController.clear();
});
},
),
title: TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Поиск по логам...',
border: InputBorder.none,
),
style: const TextStyle(color: Colors.white),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _isSearchActive ? _buildSearchAppBar() : _buildDefaultAppBar(),
body: _filteredLogEntries.isEmpty
? Center(
child: Text(
_allLogEntries.isEmpty ? "Журнал пуст." : "Записей не найдено.",
),
)
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(
8,
8,
8,
80,
), // Оставляем место для FAB
itemCount: _filteredLogEntries.length,
itemBuilder: (context, index) {
return LogEntryCard(
key: ValueKey(_filteredLogEntries[index].id),
entry: _filteredLogEntries[index],
);
},
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
onPressed: () =>
setState(() => _isAutoScrollEnabled = !_isAutoScrollEnabled),
mini: true,
tooltip: _isAutoScrollEnabled
? 'Остановить автопрокрутку'
: 'Возобновить автопрокрутку',
child: Icon(
_isAutoScrollEnabled ? Icons.pause : Icons.arrow_downward,
),
),
const SizedBox(height: 8),
FloatingActionButton.extended(
onPressed: _shareLogs,
icon: const Icon(Icons.share),
label: const Text("Поделиться"),
tooltip: "Поделиться отфильтрованными логами",
),
],
),
);
}
}
class LogEntryCard extends StatelessWidget {
final LogEntry entry;
const LogEntryCard({super.key, required this.entry});
(IconData, Color) _getVisuals(
LogType type,
String message,
BuildContext context,
) {
final theme = Theme.of(context);
switch (type) {
case LogType.send:
return (Icons.arrow_upward, theme.colorScheme.primary);
case LogType.receive:
return (Icons.arrow_downward, Colors.green);
case LogType.pingpong:
return (Icons.sync_alt, Colors.grey);
case LogType.status:
if (message.startsWith('')) return (Icons.check_circle, Colors.green);
if (message.startsWith('')) {
return (Icons.error, theme.colorScheme.error);
}
return (Icons.info, Colors.orange.shade600);
}
}
void _showJsonViewer(BuildContext context, String message) {
final jsonRegex = RegExp(r'(\{.*\})');
final match = jsonRegex.firstMatch(message);
if (match == null) return;
try {
final jsonPart = match.group(0)!;
final decoded = jsonDecode(jsonPart);
final prettyJson = const JsonEncoder.withIndent(' ').convert(decoded);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Содержимое пакета (JSON)"),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: SelectableText(
prettyJson,
style: const TextStyle(fontFamily: 'monospace'),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Закрыть"),
),
],
),
);
} catch (_) {}
}
(String?, String?) _extractInfo(String message) {
try {
final jsonRegex = RegExp(r'(\{.*\})');
final match = jsonRegex.firstMatch(message);
if (match == null) return (null, null);
final jsonPart = match.group(0)!;
final decoded = jsonDecode(jsonPart) as Map<String, dynamic>;
final opcode = decoded['opcode']?.toString();
final seq = decoded['seq']?.toString();
return (opcode, seq);
} catch (e) {
return (null, null);
}
}
@override
Widget build(BuildContext context) {
final (icon, color) = _getVisuals(entry.type, entry.message, context);
final (opcode, seq) = _extractInfo(entry.message);
final formattedTime = DateFormat('HH:mm:ss.SSS').format(entry.timestamp);
final theme = Theme.of(context);
return Card(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.symmetric(vertical: 5),
child: InkWell(
onTap: () => _showJsonViewer(context, entry.message),
child: Container(
decoration: BoxDecoration(
border: Border(left: BorderSide(color: color, width: 4)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 8),
Text(
formattedTime,
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
if (opcode != null)
Chip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
label: Text(
'OP: $opcode',
style: theme.textTheme.labelSmall,
),
),
const SizedBox(width: 4),
if (seq != null)
Chip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
label: Text(
'SEQ: $seq',
style: theme.textTheme.labelSmall,
),
),
],
),
const SizedBox(height: 8),
SelectableText(
entry.message,
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,805 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:math';
class StorageScreen extends StatefulWidget {
final bool isModal;
const StorageScreen({super.key, this.isModal = false});
@override
State<StorageScreen> createState() => _StorageScreenState();
}
class _StorageScreenState extends State<StorageScreen>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
StorageInfo? _storageInfo;
bool _isLoading = true;
Widget buildModalContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildStorageChart(colors),
const SizedBox(height: 20),
_buildStorageDetails(colors),
const SizedBox(height: 20),
_buildActionButtons(colors),
],
),
);
}
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_loadStorageInfo();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _loadStorageInfo() async {
try {
final info = await _getStorageInfo();
setState(() {
_storageInfo = info;
_isLoading = false;
});
_animationController.forward();
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
Future<StorageInfo> _getStorageInfo() async {
final appDir = await getApplicationDocumentsDirectory();
final cacheDir = await getTemporaryDirectory();
final appSize = await _getDirectorySize(appDir);
final cacheSize = await _getDirectorySize(cacheDir);
final totalSize = appSize + cacheSize;
final messagesSize = (totalSize * 0.4).round();
final mediaSize = (totalSize * 0.3).round();
final otherSize = totalSize - messagesSize - mediaSize;
return StorageInfo(
totalSize: totalSize,
messagesSize: messagesSize,
mediaSize: mediaSize,
cacheSize: cacheSize,
otherSize: otherSize,
);
}
Future<int> _getDirectorySize(Directory dir) async {
int totalSize = 0;
try {
if (await dir.exists()) {
await for (final entity in dir.list(recursive: true)) {
if (entity is File) {
totalSize += await entity.length();
}
}
}
} catch (e) {
totalSize = Random().nextInt(50) * 1024 * 1024; // 0-50 MB
}
return totalSize;
}
Future<void> _clearCache() async {
try {
final cacheDir = await getTemporaryDirectory();
if (await cacheDir.exists()) {
await cacheDir.delete(recursive: true);
await cacheDir.create();
}
await _loadStorageInfo();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Кэш успешно очищен'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при очистке кэша: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _clearAllData() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Очистить все данные'),
content: const Text(
'Это действие удалит все сообщения, медиафайлы и другие данные приложения. '
'Это действие нельзя отменить.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Удалить'),
),
],
),
);
if (confirmed == true) {
try {
final appDir = await getApplicationDocumentsDirectory();
final cacheDir = await getTemporaryDirectory();
if (await appDir.exists()) {
await appDir.delete(recursive: true);
await appDir.create();
}
if (await cacheDir.exists()) {
await cacheDir.delete(recursive: true);
await cacheDir.create();
}
await _loadStorageInfo();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Все данные успешно удалены'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при удалении данных: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024)
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (widget.isModal) {
return buildModalContent(context);
}
return Scaffold(
appBar: AppBar(
title: const Text('Хранилище'),
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
elevation: 0,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _storageInfo == null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.storage_outlined,
size: 64,
color: colors.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'Не удалось загрузить информацию о хранилище',
style: TextStyle(
color: colors.onSurface.withOpacity(0.6),
fontSize: 16,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadStorageInfo,
child: const Text('Повторить'),
),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStorageChart(colors),
const SizedBox(height: 32),
_buildStorageDetails(colors),
const SizedBox(height: 32),
_buildActionButtons(colors),
],
),
),
);
}
Widget _buildModalSettings(BuildContext context, ColorScheme colors) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
),
Center(
child: Container(
width: 400,
height: 600,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
tooltip: 'Назад',
),
const Expanded(
child: Text(
"Хранилище",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: 'Закрыть',
),
],
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildStorageChart(colors),
const SizedBox(height: 20),
_buildStorageDetails(colors),
const SizedBox(height: 20),
_buildActionButtons(colors),
],
),
),
),
],
),
),
),
],
),
);
}
Widget _buildStorageChart(ColorScheme colors) {
final totalSize = _storageInfo!.totalSize;
final usedSize =
_storageInfo!.messagesSize +
_storageInfo!.mediaSize +
_storageInfo!.otherSize;
final usagePercentage = totalSize > 0 ? usedSize / totalSize : 0.0;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
children: [
Text(
'Использование хранилища',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 24),
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return SizedBox(
width: 200,
height: 200,
child: Stack(
children: [
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.surfaceContainerHighest,
),
),
CustomPaint(
size: const Size(200, 200),
painter: StorageChartPainter(
progress: usagePercentage * _animation.value,
colors: colors,
storageInfo: _storageInfo!,
animationValue: _animation.value,
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatBytes(usedSize),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colors.primary,
),
),
Text(
'из ${_formatBytes(totalSize)}',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withOpacity(0.7),
),
),
],
),
),
],
),
);
},
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildLegendItem(
'Сообщения',
_formatBytes(_storageInfo!.messagesSize),
Colors.blue,
),
_buildLegendItem(
'Медиафайлы',
_formatBytes(_storageInfo!.mediaSize),
Colors.green,
),
_buildLegendItem(
'Кэш',
_formatBytes(_storageInfo!.cacheSize),
Colors.orange,
),
_buildLegendItem(
'Другие',
_formatBytes(_storageInfo!.otherSize),
Colors.grey,
),
],
),
],
),
);
}
Widget _buildLegendItem(String label, String value, Color color) {
return Column(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 12)),
Text(
value,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
],
);
}
Widget _buildStorageDetails(ColorScheme colors) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colors.outline.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Детали использования',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
_buildStorageItem(
'Сообщения',
_formatBytes(_storageInfo!.messagesSize),
Icons.message_outlined,
colors.primary,
(_storageInfo!.messagesSize / _storageInfo!.totalSize),
),
_buildStorageItem(
'Медиафайлы',
_formatBytes(_storageInfo!.mediaSize),
Icons.photo_library_outlined,
colors.secondary,
(_storageInfo!.mediaSize / _storageInfo!.totalSize),
),
_buildStorageItem(
'Кэш',
_formatBytes(_storageInfo!.cacheSize),
Icons.cached,
colors.tertiary,
(_storageInfo!.cacheSize / _storageInfo!.totalSize),
),
_buildStorageItem(
'Другие данные',
_formatBytes(_storageInfo!.otherSize),
Icons.folder_outlined,
colors.outline,
(_storageInfo!.otherSize / _storageInfo!.totalSize),
),
],
),
);
}
Widget _buildStorageItem(
String title,
String size,
IconData icon,
Color color,
double percentage,
) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.zero,
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
),
const SizedBox(height: 2),
Text(
size,
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withOpacity(0.6),
),
),
],
),
),
Container(
width: 60,
height: 4,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.zero,
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: percentage,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.zero,
),
),
),
),
],
),
);
}
Widget _buildActionButtons(ColorScheme colors) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Действия',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _clearCache,
icon: const Icon(Icons.cleaning_services_outlined),
label: const Text('Очистить кэш'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _clearAllData,
icon: const Icon(Icons.delete_forever_outlined),
label: const Text('Очистить всё'),
style: ElevatedButton.styleFrom(
backgroundColor: colors.error,
foregroundColor: colors.onError,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
);
}
}
class StorageInfo {
final int totalSize;
final int messagesSize;
final int mediaSize;
final int cacheSize;
final int otherSize;
StorageInfo({
required this.totalSize,
required this.messagesSize,
required this.mediaSize,
required this.cacheSize,
required this.otherSize,
});
}
class StorageChartPainter extends CustomPainter {
final double progress;
final ColorScheme colors;
final StorageInfo storageInfo;
final double animationValue;
StorageChartPainter({
required this.progress,
required this.colors,
required this.storageInfo,
required this.animationValue,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 8;
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 24 // Увеличиваем толщину с 16 до 24
..strokeCap = StrokeCap.round;
paint.color = colors.surfaceContainerHighest;
canvas.drawCircle(center, radius, paint);
final totalSize = storageInfo.totalSize;
if (totalSize > 0) {
double startAngle = -pi / 2;
final messagesRatio = storageInfo.messagesSize / totalSize;
final mediaRatio = storageInfo.mediaSize / totalSize;
final cacheRatio = storageInfo.cacheSize / totalSize;
final otherRatio = storageInfo.otherSize / totalSize;
if (messagesRatio > 0) {
paint.color = Colors.blue;
final sweepAngle = 2 * pi * messagesRatio * animationValue;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
startAngle += 2 * pi * messagesRatio; // Обновляем без анимации
}
if (mediaRatio > 0) {
paint.color = Colors.green;
final sweepAngle = 2 * pi * mediaRatio * animationValue;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
startAngle += 2 * pi * mediaRatio; // Обновляем без анимации
}
if (cacheRatio > 0) {
paint.color = Colors.orange;
final sweepAngle = 2 * pi * cacheRatio * animationValue;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
startAngle += 2 * pi * cacheRatio; // Обновляем без анимации
}
if (otherRatio > 0) {
paint.color = Colors.grey;
final sweepAngle = 2 * pi * otherRatio * animationValue;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is StorageChartPainter &&
(oldDelegate.progress != progress ||
oldDelegate.animationValue != animationValue);
}
}