Files
eatvio_app/lib/features/dev_tools/presentation/dev_tools_screen.dart

516 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/features/diary/presentation/pages/barcode_scanner_screen.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
class DevToolsScreen extends StatelessWidget {
const DevToolsScreen({super.key});
static const name = 'devToolsScreen';
static const path = '/dev-tools-screen';
@override
Widget build(BuildContext context) {
return const DevToolsScreenContent();
}
}
class DevToolsScreenContent extends StatefulWidget {
const DevToolsScreenContent({super.key});
@override
State<DevToolsScreenContent> createState() => _DevToolsScreenContentState();
}
class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
String baseUrlForTest = '';
final List<TestCases> testCases = [
Test850task(),
GetChatRoomPreviews(),
GetChatRoom(),
SendMessage(),
GetChatMessages(),
GetChatRoomIdByUserSlug(),
GetBlogPostList(),
GetRecipes(),
CreateRecipeTask(),
GetDiets(),
GetPlans(),
GetBlogPostComments(),
SendBlogPostComment(),
GetDietsComments(),
SendDietsComment(),
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
AuthServiceTest(),
DiaryServiceTest(),
BlockedUserServiceTest(),
PlanServiceTest(),
AboutRecipeServiceTest(),
BlogPostServiceTest(),
CalendarServiceTest(),
CardsServiceTest(),
ConsultantServiceTest(),
ConsultationServiceTest(),
CreateDietServiceTest(),
CreatePlanServiceTest(),
GlossaryServiceTest(),
MyDietsServiceTest(),
MyPlansServiceTest(),
MyRecipesServiceTest(),
NotificationsServiceTest(),
PlansServiceTest(),
ProfileServiceTest(),
RegistrationServiceTest(),
ResetPasswordServiceTest(),
SettingsServiceTest(),
WsServiceTest(),
];
final controller = TextEditingController();
late final List<TestState> testsStates;
@override
void initState() {
super.initState();
setState(() {
baseUrlForTest = sl<AppConfig>().baseUrl;
});
testsStates = List.generate(testCases.length, (index) => TestState.notRun);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Для разработчиков')),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [Text('BaseUrl: $baseUrlForTest')]),
),
Row(
children: [
Expanded(
child: CustomTextFormField(controller: controller, hintText: 'type new base url'),
),
IconButton(
onPressed: () {
setState(() {
baseUrlForTest = controller.text;
sl<AppConfig>().setBaseUrl(baseUrlForTest.toLowerCase());
sl<Dio>().options.baseUrl = baseUrlForTest.toLowerCase();
controller.clear();
});
},
icon: const Icon(Icons.done),
),
],
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: sl<AppConfig>().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
onPressed: () {
setState(() {
if (sl<AppConfig>().baseUrl == EnvConfig.baseUrl) {
baseUrlForTest = EnvConfig.devBaseUrl;
} else {
baseUrlForTest = EnvConfig.baseUrl;
}
sl<AppConfig>().setBaseUrl(baseUrlForTest.toLowerCase());
sl<Dio>().options.baseUrl = baseUrlForTest.toLowerCase();
controller.clear();
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != sl<AppConfig>().baseUrl ? 40 : 10),
if (sl<AppConfig>().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomOutlinedButton(
text: 'run',
height: 20,
width: 60,
onPressed: () async {
if (testsStates[index] == TestState.loading) return;
setState(() {
testsStates[index] = TestState.loading;
});
try {
final result = await testCases[index].run(baseUrl: baseUrlForTest);
if (result.success) {
setState(() {
testsStates[index] = TestState.success;
});
showSuccessToast('${testCases[index].name} passed');
} else {
setState(() {
testsStates[index] = TestState.fail;
});
showErrorToast('${testCases[index].name} failed');
// Show AlertDialog with error description
if (context.mounted) {
showDialog(
context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
}
} catch (e, stack) {
setState(() {
testsStates[index] = TestState.fail;
});
showErrorToast('${testCases[index].name} failed');
// Show AlertDialog with error description
if (context.mounted) {
showDialog(
context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text('$e\n$stack')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
}
},
buttonStyle: const ButtonStyle(),
),
const SizedBox(width: 5),
switch (testsStates[index]) {
TestState.success => Container(width: 20, height: 20, color: Colors.green),
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
},
],
),
],
),
),
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'add token to server',
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final dio = sl<Dio>();
final String platform;
if (Platform.isAndroid) {
platform = 'android';
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/register', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен добавлен');
} catch (e) {
showInfoToast('Не удалось добавть токен');
}
},
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'copy token',
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final token = await sl<FirebaseApi>().getToken();
if (token != null) {
Clipboard.setData(ClipboardData(text: token));
showInfoToast('Токен скопирован');
} else {
showInfoToast('Token is null');
}
} catch (e) {
showInfoToast('Не получилось скопировать токен');
}
},
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'remove token from server',
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final dio = sl<Dio>();
final String platform;
if (Platform.isAndroid) {
platform = 'android';
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/unregister', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен удалён');
} catch (e) {
showInfoToast('Не удалось удалить токен');
}
},
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'send notification',
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final dio = sl<Dio>();
Future.delayed(const Duration(seconds: 3), () async {
await dio.post('/api/profile/notification/token/test');
});
} catch (e) {
showInfoToast('Не удалось добавть токен');
}
},
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'Сканирование QR/штрихкода',
buttonStyle: const ButtonStyle(),
onPressed: () async {
if (!context.mounted) return;
final barcode = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()),
);
if (barcode != null && barcode.isNotEmpty && context.mounted) {
await _handleBarcodeScanned(barcode, context);
}
},
),
const SizedBox(height: 300),
],
),
),
),
);
}
Future<void> _handleBarcodeScanned(String barcode, BuildContext context) async {
try {
final barcodeRepository = sl<BarcodeRepository>();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Загрузка данных...'),
],
),
),
);
final result = await barcodeRepository.getProductByBarcode(barcode);
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
if (result.type == BarcodeResultType.success && result.product != null) {
// Данные получены сразу
_showProductDialog(context, result.product!, barcode);
} else if (result.type == BarcodeResultType.processing && result.taskId != null) {
// Запущена задача обработки, нужно опрашивать статус
await _pollTaskStatus(context, result.taskId!, barcode);
}
} catch (e) {
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки если открыт
showErrorToast('Ошибка: $e');
_showBarcodeDialog(context, barcode, null);
}
}
Future<void> _pollTaskStatus(BuildContext context, String taskId, String barcode) async {
final barcodeRepository = sl<BarcodeRepository>();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
const Text('Обработка штрихкода...'),
const SizedBox(height: 8),
Text('Это может занять некоторое время', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
),
);
final completer = Completer<void>();
int attempts = 0;
const maxAttempts = 30; // Максимум 30 попыток (примерно 5 минут при интервале 10 секунд)
// ignore: unused_local_variable
final timer = Timer.periodic(const Duration(seconds: 10), (timer) async {
attempts++;
try {
final status = await barcodeRepository.getTaskStatus(taskId);
if (status.type == BarcodeTaskStatusType.completed && status.product != null) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
_showProductDialog(context, status.product!, barcode);
completer.complete();
} else if (status.type == BarcodeTaskStatusType.failed) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Не удалось получить данные о продукте');
_showBarcodeDialog(context, barcode, null);
completer.complete();
} else if (attempts >= maxAttempts) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Превышено время ожидания');
_showBarcodeDialog(context, barcode, null);
completer.complete();
}
} catch (e) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Ошибка при проверке статуса: $e');
_showBarcodeDialog(context, barcode, null);
completer.complete();
}
});
await completer.future;
}
void _showProductDialog(BuildContext context, BarcodeProductModel product, String barcode) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Информация о продукте'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildInfoRow('Штрихкод:', barcode),
if (product.brandName != null) _buildInfoRow('Бренд:', product.brandName!),
if (product.description != null) _buildInfoRow('Описание:', product.description!),
if (product.quantity != null) _buildInfoRow('Количество:', product.quantity!),
if (product.market != null) _buildInfoRow('Рынок сбыта:', product.market!),
if (product.calories != null) _buildInfoRow('Калорийность:', '${product.calories} ккал'),
if (product.proteins != null) _buildInfoRow('Белки:', '${product.proteins} г'),
if (product.fats != null) _buildInfoRow('Жиры:', '${product.fats} г'),
if (product.carbohydrates != null) _buildInfoRow('Углеводы:', '${product.carbohydrates} г'),
],
),
),
actions: [
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: barcode));
showInfoToast('Штрихкод скопирован в буфер обмена');
},
child: const Text('Скопировать код'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Закрыть'),
),
],
),
);
}
void _showBarcodeDialog(BuildContext context, String barcode, BarcodeProductModel? product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Отсканированный код'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(barcode),
if (product == null) ...[
const SizedBox(height: 16),
const Text('Данные о продукте не найдены', style: TextStyle(color: Colors.grey)),
],
],
),
actions: [
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: barcode));
Navigator.of(context).pop();
showInfoToast('Код скопирован в буфер обмена');
},
child: const Text('Скопировать'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Закрыть'),
),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black, fontSize: 14),
children: [
TextSpan(text: '$label ', style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: value),
],
),
),
);
}
}
enum TestState { success, fail, notRun, loading }