741 lines
25 KiB
Dart
741 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter/scheduler.dart';
|
||
import 'package:gwid/cache_management_screen.dart'; // Добавлен импорт
|
||
import 'package:provider/provider.dart';
|
||
import 'package:gwid/theme_provider.dart';
|
||
import 'package:gwid/api/api_service.dart';
|
||
import 'package:gwid/phone_entry_screen.dart';
|
||
import 'package:gwid/custom_request_screen.dart';
|
||
import 'dart:async';
|
||
|
||
class DebugScreen extends StatelessWidget {
|
||
const DebugScreen({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('Debug Settings'),
|
||
backgroundColor: colors.surface,
|
||
foregroundColor: colors.onSurface,
|
||
),
|
||
body: ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
_OutlinedSection(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 8.0),
|
||
child: Text(
|
||
"Performance Debug",
|
||
style: TextStyle(
|
||
color: colors.primary,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 18,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.speed),
|
||
title: const Text("Показать FPS overlay"),
|
||
subtitle: const Text("Отображение FPS и производительности"),
|
||
trailing: Switch(
|
||
value: theme.debugShowPerformanceOverlay,
|
||
onChanged: (value) =>
|
||
theme.setDebugShowPerformanceOverlay(value),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.refresh),
|
||
title: const Text("Показать панель обновления чатов"),
|
||
subtitle: const Text(
|
||
"Debug панель для обновления списка чатов",
|
||
),
|
||
trailing: Switch(
|
||
value: theme.debugShowChatsRefreshPanel,
|
||
onChanged: (value) =>
|
||
theme.setDebugShowChatsRefreshPanel(value),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.message),
|
||
title: const Text("Показать счётчик сообщений"),
|
||
subtitle: const Text("Отладочная информация о сообщениях"),
|
||
trailing: Switch(
|
||
value: theme.debugShowMessageCount,
|
||
onChanged: (value) => theme.setDebugShowMessageCount(value),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
_OutlinedSection(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 8.0),
|
||
child: Text(
|
||
"Инструменты разработчика",
|
||
style: TextStyle(
|
||
color: colors.primary,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 18,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.code),
|
||
title: const Text("Custom API Request"),
|
||
subtitle: const Text("Отправить сырой запрос на сервер"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (context) => const CustomRequestScreen(),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
_OutlinedSection(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 8.0),
|
||
child: Text(
|
||
"Data Management",
|
||
style: TextStyle(
|
||
color: colors.primary,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 18,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.delete_forever),
|
||
title: const Text("Очистить все данные"),
|
||
subtitle: const Text("Полная очистка кэшей и данных"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () => _showClearAllDataDialog(context),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.phone),
|
||
title: const Text("Показать ввод номера"),
|
||
subtitle: const Text("Открыть экран ввода номера без выхода"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () => _showPhoneEntryScreen(context),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.traffic),
|
||
title: const Text("Статистика трафика"),
|
||
subtitle: const Text("Просмотр использованного трафика"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () => _showTrafficStats(context),
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.storage),
|
||
title: const Text("Использование памяти"),
|
||
subtitle: const Text("Просмотр статистики памяти"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () => _showMemoryUsage(context),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.cached),
|
||
title: const Text("Управление кэшем"),
|
||
subtitle: const Text("Настройки кэширования и статистика"),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (context) => const CacheManagementScreen(),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showClearAllDataDialog(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('Очистить все данные'),
|
||
content: const Text(
|
||
'Это действие удалит ВСЕ данные приложения:\n\n'
|
||
'• Все кэши и сообщения\n'
|
||
'• Настройки и профиль\n'
|
||
'• Токен авторизации\n'
|
||
'• История чатов\n\n'
|
||
'После очистки приложение будет закрыто.\n'
|
||
'Вы уверены?',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Отмена'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () async {
|
||
Navigator.of(context).pop();
|
||
await _performFullDataClear(context);
|
||
},
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: Colors.red,
|
||
foregroundColor: Colors.white,
|
||
),
|
||
child: const Text('Очистить и закрыть'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _performFullDataClear(BuildContext context) async {
|
||
try {
|
||
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) => const AlertDialog(
|
||
content: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
CircularProgressIndicator(),
|
||
SizedBox(width: 16),
|
||
Text('Очистка данных...'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
|
||
await ApiService.instance.clearAllData();
|
||
|
||
|
||
if (context.mounted) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
|
||
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Все данные очищены. Приложение будет закрыто.'),
|
||
backgroundColor: Colors.green,
|
||
duration: Duration(seconds: 2),
|
||
),
|
||
);
|
||
}
|
||
|
||
|
||
await Future.delayed(const Duration(seconds: 2));
|
||
|
||
|
||
if (context.mounted) {
|
||
SystemNavigator.pop();
|
||
}
|
||
} catch (e) {
|
||
|
||
if (context.mounted) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
|
||
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('Ошибка при очистке данных: $e'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _showPhoneEntryScreen(BuildContext context) {
|
||
Navigator.of(
|
||
context,
|
||
).push(MaterialPageRoute(builder: (context) => const PhoneEntryScreen()));
|
||
}
|
||
|
||
void _showTrafficStats(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('Статистика трафика'),
|
||
content: const Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('📊 Статистика использования данных:'),
|
||
SizedBox(height: 16),
|
||
Text('• Отправлено сообщений: 1,247'),
|
||
Text('• Получено сообщений: 3,891'),
|
||
Text('• Загружено фото: 156 MB'),
|
||
Text('• Загружено видео: 89 MB'),
|
||
Text('• Общий трафик: 2.1 GB'),
|
||
SizedBox(height: 16),
|
||
Text('📅 За последние 30 дней'),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Закрыть'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showMemoryUsage(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('Использование памяти'),
|
||
content: const Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('💾 Использование памяти:'),
|
||
SizedBox(height: 16),
|
||
Text('• Кэш сообщений: 45.2 MB'),
|
||
Text('• Кэш контактов: 12.8 MB'),
|
||
Text('• Кэш чатов: 8.3 MB'),
|
||
Text('• Медиа файлы: 156.7 MB'),
|
||
Text('• Общее использование: 223.0 MB'),
|
||
SizedBox(height: 16),
|
||
Text('📱 Доступно памяти: 2.1 GB'),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Закрыть'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
ApiService.instance.clearAllCaches();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Кэш очищен'),
|
||
backgroundColor: Colors.green,
|
||
),
|
||
);
|
||
},
|
||
child: const Text('Очистить кэш'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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,
|
||
);
|
||
}
|
||
}
|
||
|
||
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;
|
||
bool _isInitialLoad = true;
|
||
StreamSubscription? _apiSubscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_listenToApi();
|
||
|
||
}
|
||
|
||
void _loadSessions() {
|
||
|
||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
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,
|
||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||
),
|
||
child: const Text('Сбросить'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed == true) {
|
||
|
||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isLoading = true;
|
||
});
|
||
}
|
||
});
|
||
|
||
ApiService.instance.terminateAllSessions();
|
||
|
||
|
||
Future.delayed(const Duration(seconds: 2), () {
|
||
if (mounted) {
|
||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
_loadSessions();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
void _listenToApi() {
|
||
_apiSubscription = ApiService.instance.messages.listen((message) {
|
||
if (message['opcode'] == 96 && mounted) {
|
||
|
||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
});
|
||
|
||
final payload = message['payload'];
|
||
if (payload != null && payload['sessions'] != null) {
|
||
final sessionsList = payload['sessions'] as List;
|
||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_sessions = sessionsList
|
||
.map((session) => Session.fromJson(session))
|
||
.toList();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
String _formatTime(int timestamp) {
|
||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||
final now = DateTime.now();
|
||
final difference = now.difference(date);
|
||
|
||
String relativeTime;
|
||
if (difference.inDays > 0) {
|
||
relativeTime = '${difference.inDays} дн. назад';
|
||
} else if (difference.inHours > 0) {
|
||
relativeTime = '${difference.inHours} ч. назад';
|
||
} else if (difference.inMinutes > 0) {
|
||
relativeTime = '${difference.inMinutes} мин. назад';
|
||
} else {
|
||
relativeTime = 'Только что';
|
||
}
|
||
|
||
|
||
final exactTime =
|
||
'${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||
|
||
return '$relativeTime ($exactTime)';
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colors = Theme.of(context).colorScheme;
|
||
|
||
|
||
if (_isInitialLoad && _sessions.isEmpty) {
|
||
_isInitialLoad = false;
|
||
_loadSessions();
|
||
}
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text("Активные сессии"),
|
||
actions: [
|
||
IconButton(onPressed: _loadSessions, icon: const Icon(Icons.refresh)),
|
||
],
|
||
),
|
||
body: _isLoading
|
||
? const Center(child: CircularProgressIndicator())
|
||
: _sessions.isEmpty
|
||
? Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.security,
|
||
size: 64,
|
||
color: colors.onSurfaceVariant,
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
"Нет активных сессий",
|
||
style: TextStyle(
|
||
color: colors.onSurfaceVariant,
|
||
fontSize: 18,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: Column(
|
||
children: [
|
||
|
||
if (_sessions.any((s) => !s.current))
|
||
Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.all(16),
|
||
child: FilledButton.icon(
|
||
onPressed: _terminateAllSessions,
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: colors.error,
|
||
foregroundColor: colors.onError,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 24,
|
||
vertical: 16,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
icon: const Icon(Icons.logout, size: 24),
|
||
label: const Text(
|
||
"Завершить все сессии кроме текущей",
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
Expanded(
|
||
child: ListView.builder(
|
||
padding: const EdgeInsets.all(16),
|
||
itemCount: _sessions.length,
|
||
itemBuilder: (context, index) {
|
||
final session = _sessions[index];
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
child: ListTile(
|
||
leading: CircleAvatar(
|
||
backgroundColor: session.current
|
||
? colors.primary
|
||
: colors.surfaceContainerHighest,
|
||
child: Icon(
|
||
session.current
|
||
? Icons.phone_android
|
||
: Icons.computer,
|
||
color: session.current
|
||
? colors.onPrimary
|
||
: colors.onSurfaceVariant,
|
||
),
|
||
),
|
||
title: Text(
|
||
session.current ? "Текущая сессия" : session.client,
|
||
style: TextStyle(
|
||
fontWeight: session.current
|
||
? FontWeight.bold
|
||
: FontWeight.normal,
|
||
color: session.current
|
||
? colors.primary
|
||
: colors.onSurface,
|
||
),
|
||
),
|
||
subtitle: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
session.location,
|
||
style: TextStyle(
|
||
color: colors.onSurfaceVariant,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
session.info,
|
||
style: TextStyle(
|
||
color: colors.onSurfaceVariant,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
_formatTime(session.time),
|
||
style: TextStyle(
|
||
color: colors.onSurfaceVariant,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
trailing: session.current
|
||
? Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8,
|
||
vertical: 4,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: colors.primary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text(
|
||
"Активна",
|
||
style: TextStyle(
|
||
color: colors.onPrimary,
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
)
|
||
: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8,
|
||
vertical: 4,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: colors.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text(
|
||
"Неактивна",
|
||
style: TextStyle(
|
||
color: colors.onSurfaceVariant,
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_apiSubscription?.cancel();
|
||
super.dispose();
|
||
}
|
||
}
|