Initial Commit
This commit is contained in:
1206
lib/screens/group_settings_screen.dart
Normal file
1206
lib/screens/group_settings_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
440
lib/screens/settings/about_screen.dart
Normal file
440
lib/screens/settings/about_screen.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
370
lib/screens/settings/animations_screen.dart
Normal file
370
lib/screens/settings/animations_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
325
lib/screens/settings/appearance_settings_screen.dart
Normal file
325
lib/screens/settings/appearance_settings_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
331
lib/screens/settings/bypass_screen.dart
Normal file
331
lib/screens/settings/bypass_screen.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
1503
lib/screens/settings/customization_screen.dart
Normal file
1503
lib/screens/settings/customization_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
278
lib/screens/settings/export_session_screen.dart
Normal file
278
lib/screens/settings/export_session_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
521
lib/screens/settings/komet_misc_screen.dart
Normal file
521
lib/screens/settings/komet_misc_screen.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
868
lib/screens/settings/network_screen.dart
Normal file
868
lib/screens/settings/network_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
262
lib/screens/settings/network_settings_screen.dart
Normal file
262
lib/screens/settings/network_settings_screen.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
591
lib/screens/settings/notification_settings_screen.dart
Normal file
591
lib/screens/settings/notification_settings_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
216
lib/screens/settings/privacy_security_screen.dart
Normal file
216
lib/screens/settings/privacy_security_screen.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
383
lib/screens/settings/privacy_settings_screen.dart
Normal file
383
lib/screens/settings/privacy_settings_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
263
lib/screens/settings/proxy_settings_screen.dart
Normal file
263
lib/screens/settings/proxy_settings_screen.dart
Normal 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 ? 'Проверка...' : 'Проверить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
301
lib/screens/settings/qr_login_screen.dart
Normal file
301
lib/screens/settings/qr_login_screen.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/screens/settings/qr_scanner_screen.dart
Normal file
64
lib/screens/settings/qr_scanner_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
lib/screens/settings/reconnection_screen.dart
Normal file
298
lib/screens/settings/reconnection_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
91
lib/screens/settings/security_settings_screen.dart
Normal file
91
lib/screens/settings/security_settings_screen.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
754
lib/screens/settings/session_spoofing_screen.dart
Normal file
754
lib/screens/settings/session_spoofing_screen.dart
Normal 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('Применить'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
251
lib/screens/settings/sessions_screen.dart
Normal file
251
lib/screens/settings/sessions_screen.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
610
lib/screens/settings/settings_screen.dart
Normal file
610
lib/screens/settings/settings_screen.dart
Normal 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));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
513
lib/screens/settings/socket_log_screen.dart
Normal file
513
lib/screens/settings/socket_log_screen.dart
Normal 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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
805
lib/screens/settings/storage_screen.dart
Normal file
805
lib/screens/settings/storage_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user