fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type

This commit is contained in:
mso7nueh
2025-12-22 16:48:13 +03:00
parent f56092d88f
commit 42b443c3ba
61 changed files with 759 additions and 393 deletions

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
@@ -55,6 +56,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));

View File

@@ -19,6 +19,7 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
@@ -56,6 +57,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));

View File

@@ -25,3 +25,5 @@ abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmR

View File

@@ -19,3 +19,5 @@ abstract class OAuthVerificationRequest with _$OAuthVerificationRequest {

View File

@@ -19,3 +19,5 @@ abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestR

View File

@@ -338,7 +338,13 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
children: [
const Text('Вес'),
const SizedBox(height: 4),
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
CustomTextFormField(
controller: weightController,
hintText: 'Введите вес',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: weightError,
),
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
],
),
@@ -351,7 +357,13 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
children: [
const Text('Рост'),
const SizedBox(height: 4),
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
CustomTextFormField(
controller: heightController,
hintText: 'Введите рост',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: heightError,
),
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
],
),

View File

@@ -0,0 +1,29 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'barcode_product_model.freezed.dart';
part 'barcode_product_model.g.dart';
@freezed
abstract class BarcodeProductModel with _$BarcodeProductModel {
const factory BarcodeProductModel({
// ignore: non_constant_identifier_names
required String GTIN,
@JsonKey(name: 'Товарный знак (ru)') String? brandName,
@JsonKey(name: 'Описание (ru)') String? description,
@JsonKey(name: 'Количество/вес') String? quantity,
@JsonKey(name: 'Рынок сбыта') String? market,
@JsonKey(name: 'Калорийность') double? calories,
@JsonKey(name: 'Белки') double? proteins,
@JsonKey(name: 'Жиры') double? fats,
@JsonKey(name: 'Углеводы') double? carbohydrates,
}) = _BarcodeProductModel;
factory BarcodeProductModel.fromJson(Map<String, Object?> json) => _$BarcodeProductModelFromJson(json);
}
@freezed
abstract class BarcodeTaskStatusModel with _$BarcodeTaskStatusModel {
const factory BarcodeTaskStatusModel({String? status, @JsonKey(name: 'task_id') String? taskId, BarcodeProductModel? data}) = _BarcodeTaskStatusModel;
factory BarcodeTaskStatusModel.fromJson(Map<String, Object?> json) => _$BarcodeTaskStatusModelFromJson(json);
}

View File

@@ -0,0 +1,108 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
class BarcodeRepository {
final BarcodeService barcodeService;
final Dio dio;
BarcodeRepository({
required this.barcodeService,
required this.dio,
});
/// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async {
try {
final response = await dio.get('/api/gsone/barcode/$barcode');
if (response.statusCode == 200) {
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.success(product);
} else if (response.statusCode == 202) {
// Запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
rethrow;
} catch (e) {
rethrow;
}
}
/// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try {
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
} catch (e) {
rethrow;
}
}
}
/// Результат запроса информации о продукте по штрихкоду
class BarcodeResult {
final BarcodeResultType type;
final BarcodeProductModel? product;
final String? taskId;
BarcodeResult._({required this.type, this.product, this.taskId});
factory BarcodeResult.success(BarcodeProductModel product) {
return BarcodeResult._(type: BarcodeResultType.success, product: product);
}
factory BarcodeResult.processing(String taskId) {
return BarcodeResult._(type: BarcodeResultType.processing, taskId: taskId);
}
}
enum BarcodeResultType {
success,
processing,
}
/// Статус задачи обработки
class BarcodeTaskStatus {
final BarcodeTaskStatusType type;
final BarcodeProductModel? product;
BarcodeTaskStatus._({required this.type, this.product});
factory BarcodeTaskStatus.completed(BarcodeProductModel product) {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.completed, product: product);
}
factory BarcodeTaskStatus.processing() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.processing);
}
factory BarcodeTaskStatus.failed() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.failed);
}
}
enum BarcodeTaskStatusType {
completed,
processing,
failed,
}

View File

@@ -0,0 +1,17 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:retrofit/retrofit.dart';
part 'barcode_service.g.dart';
@RestApi()
abstract class BarcodeService {
factory BarcodeService(Dio dio, {String baseUrl}) = _BarcodeService;
@GET('/api/gsone/barcode/{barcode}')
Future<BarcodeProductModel> getProductByBarcode({@Path('barcode') required String barcode});
@GET('/api/gsone/barcode/status/{task_id}')
Future<BarcodeTaskStatusModel> getTaskStatus({@Path('task_id') required String taskId});
}

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -79,7 +78,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -195,7 +197,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
padding: EdgeInsets.only(bottom: 160 + MediaQuery.of(context).padding.bottom), // поле комментария (52-200) + padding (12+12+56) + safe area
physics: const BouncingScrollPhysics(),
child: Column(
children: [

View File

@@ -27,6 +27,7 @@ abstract class ChatMessageModel with _$ChatMessageModel {
required int id,
@JsonKey(name: 'data', fromJson: _parseData) required String data,
@JsonKey(name: 'media_data') required List<dynamic>? mediaData,
@JsonKey(name: 'urls_media_info') List<Map<String, dynamic>>? urlsMediaInfo,
@JsonKey(name: 'author_id', fromJson: _parseAuthorId) required int authorId,
@JsonKey(name: 'updated_at', fromJson: _parseUpdatedAt) required DateTime updatedAt,
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,

View File

@@ -275,6 +275,7 @@ class _ChatScreenState extends State<ChatScreen> {
userSlug: state.chatRoom.users.receiver.slug,
messageType: message.type,
isConsultant: amIConsultant,
urlsMediaInfo: message.urlsMediaInfo,
),
),
],
@@ -330,6 +331,7 @@ class _ChatScreenState extends State<ChatScreen> {
userSlug: state.chatRoom.users.receiver.slug,
messageType: message.type,
isConsultant: amIConsultant,
urlsMediaInfo: message.urlsMediaInfo,
),
),
],
@@ -460,34 +462,30 @@ class _ChatScreenState extends State<ChatScreen> {
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: 3,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),

View File

@@ -79,34 +79,30 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: 3,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),

View File

@@ -28,6 +28,7 @@ class ChatBubbleWidget extends StatelessWidget {
required this.userSlug,
this.messageType,
this.isConsultant,
this.urlsMediaInfo,
});
final DateTime sendTime;
@@ -40,6 +41,7 @@ class ChatBubbleWidget extends StatelessWidget {
final String userSlug;
final String? messageType;
final bool? isConsultant;
final List<Map<String, dynamic>>? urlsMediaInfo;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
@@ -127,7 +129,57 @@ class ChatBubbleWidget extends StatelessWidget {
fileName = mediaItem.path.split('/').last;
} else {
final urlString = mediaItem.toString();
fileName = urlString.contains('/') ? urlString.split('/').last.split('?').first : urlString.replaceAll('chat_files/', '');
// Получаем расширение файла из URL
String? fileExtension;
final urlParts = urlString.split('/');
if (urlParts.isNotEmpty) {
final lastPart = urlParts.last.split('?').first;
final dotIndex = lastPart.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < lastPart.length - 1) {
fileExtension = lastPart.substring(dotIndex);
}
}
// Пытаемся найти имя файла в urls_media_info
String? fileNameFromInfo;
final mediaInfo = urlsMediaInfo;
if (mediaInfo != null && mediaInfo.isNotEmpty) {
for (final mediaInfoMap in mediaInfo) {
// Каждый элемент urls_media_info - это Map, где ключ - это URL
if (mediaInfoMap.isNotEmpty) {
for (final entry in mediaInfoMap.entries) {
final infoUrl = entry.key.toString();
// Сравниваем полные URL или только имя файла
if (infoUrl == urlString ||
urlString.contains(infoUrl) ||
infoUrl.contains(urlString) ||
urlString.split('/').last == infoUrl.split('/').last) {
final fileInfo = entry.value;
if (fileInfo is Map<String, dynamic> && fileInfo.containsKey('name')) {
final nameValue = fileInfo['name'];
if (nameValue != null) {
fileNameFromInfo = nameValue.toString();
break;
}
}
}
}
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) break;
}
}
}
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) {
// Добавляем расширение к названию файла, если его там нет
if (fileExtension != null && !fileNameFromInfo.endsWith(fileExtension)) {
fileName = '$fileNameFromInfo$fileExtension';
} else {
fileName = fileNameFromInfo;
}
} else {
// Fallback к старой логике, если информации нет
fileName = urlString.contains('/') ? urlString.split('/').last.split('?').first : urlString.replaceAll('chat_files/', '');
}
}
return ListTile(

View File

@@ -10,8 +10,12 @@ 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});
@@ -296,6 +300,20 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
}
},
),
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),
],
),
@@ -303,6 +321,195 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
),
);
}
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 }

View File

@@ -25,61 +25,63 @@ class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> {
title: const Text('Сканирование штрихкода'),
centerTitle: true,
),
body: Column(
children: [
Expanded(
child: MobileScanner(
controller: _controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final rawValue = barcodes.first.rawValue;
if (rawValue == null || rawValue == _detectedBarcode) return;
setState(() {
_detectedBarcode = rawValue;
});
},
body: SafeArea(
child: Column(
children: [
Expanded(
child: MobileScanner(
controller: _controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final rawValue = barcodes.first.rawValue;
if (rawValue == null || rawValue == _detectedBarcode) return;
setState(() {
_detectedBarcode = rawValue;
});
},
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFF6F6F6),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Найденный штрихкод',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
_detectedBarcode ?? 'Наведите камеру на штрихкод',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _detectedBarcode == null ? Colors.grey : Colors.black,
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFF6F6F6),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Найденный штрихкод',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
const SizedBox(height: 16),
if (_detectedBarcode != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_detectedBarcode);
},
child: const Text('Использовать штрихкод'),
const SizedBox(height: 8),
Text(
_detectedBarcode ?? 'Наведите камеру на штрихкод',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _detectedBarcode == null ? Colors.grey : Colors.black,
),
),
],
const SizedBox(height: 16),
if (_detectedBarcode != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_detectedBarcode);
},
child: const Text('Использовать штрихкод'),
),
),
],
),
),
),
],
],
),
),
);
}

View File

@@ -5,11 +5,9 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/pages/barcode_scanner_screen.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class AddProductModal extends StatefulWidget {
final PortionModel? portion;
@@ -64,25 +62,9 @@ class _AddProductModalState extends State<AddProductModal> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Добавление блюда',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
IconButton(
onPressed: () async {
final barcode = await Navigator.of(context).push<String>(MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()));
if (barcode != null && barcode.isNotEmpty) {
productNameController.text = barcode;
setState(() {});
}
},
splashRadius: 20,
icon: const Icon(Symbols.barcode_scanner, color: Color(0xFF323F4B)),
),
],
const Text(
'Добавление блюда',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
Row(

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -70,9 +68,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
void _scrollToBottom() {
_jumped = true;
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
@override
@@ -97,11 +93,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (dietState is DietStateLoaded) {
if (_isFirstLaunch) {
context.read<FeedCubit>().viewDiet(dietState.diet.id);
context.read<CommentsCubit>().getComments(
dietState.diet.id.toString(),
CommentSourceType.diet,
true,
);
context.read<CommentsCubit>().getComments(dietState.diet.id.toString(), CommentSourceType.diet, true);
_isFirstLaunch = false;
}
Future.delayed(const Duration(milliseconds: 500), () {
@@ -113,14 +105,13 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.black.withValues(alpha: 0.1),
),
),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -136,9 +127,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: dietState.isStarted ? appTheme.transparentRed : appTheme.greenA700,
onPressed: () {
final cubit = context.read<AboutDietCubit>();
@@ -146,10 +135,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (dietState.isStarted) {
showModalBottomSheet(
context: context,
builder: (context) => CancelDietBottomSheet(
aboutDietCubit: cubit,
dietName: dietState.diet.name,
),
builder: (context) => CancelDietBottomSheet(aboutDietCubit: cubit, dietName: dietState.diet.name),
);
} else if (dietState.hasStartedDiet) {
showModalBottomSheet(
@@ -162,9 +148,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
} else {
showModalBottomSheet(
context: context,
builder: (context) => StartDietBottomSheet(
aboutDietCubit: cubit,
),
builder: (context) => StartDietBottomSheet(aboutDietCubit: cubit),
);
}
},
@@ -209,17 +193,10 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(
Icons.close_rounded,
size: 18,
color: Colors.white,
),
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
@@ -235,22 +212,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false, // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -312,14 +279,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
dietState.diet.name,
maxLines: 2,
@@ -342,7 +309,9 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -371,10 +340,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
const SizedBox(width: 8),
Text(
dietState.diet.totalViews.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
const SizedBox(width: 12),
@@ -392,10 +358,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
colorFilter: dietState.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
),
const SizedBox(width: 8),
Text(
dietState.diet.totalLikes.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
),
@@ -405,10 +368,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
const SizedBox(width: 8),
Text(
dietState.diet.totalComments.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
],
@@ -419,26 +379,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
s.about_diet,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(
diet: dietState.diet,
type: ProductType.diet,
),
ExpandableText(
text: dietState.diet.details,
),
InfoRow(diet: dietState.diet, type: ProductType.diet),
ExpandableText(text: dietState.diet.details),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: dietState.diet.author,
title: s.diet_author,
),
child: AuthorCard(author: dietState.diet.author, title: s.diet_author),
),
const SizedBox(height: 16),
],
@@ -451,9 +400,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
child: Center(
child: Text(
'${s.diet_plan}(${dietState.diet.duration.toString()} ${getDayForm(dietState.diet.duration, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
@@ -463,13 +410,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
return _dayOfPlan(
dietState.diet.dailySchedule[index + 1] ?? {},
index,
s,
dietState.selectedIndexes,
dietState.diet,
);
return _dayOfPlan(dietState.diet.dailySchedule[index + 1] ?? {}, index, s, dietState.selectedIndexes, dietState.diet);
},
itemCount: dietState.diet.dailySchedule.length,
),
@@ -482,9 +423,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
alignment: Alignment.centerLeft,
child: Text(
'${dietState.diet.totalComments} ${getCommentForm(dietState.diet.totalComments, s)}',
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray500,
),
style: TextStyles.textMMedium.copyWith(color: appTheme.gray500),
),
),
const SizedBox(height: 16),
@@ -509,14 +448,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,
@@ -569,18 +508,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
label: Text((index + 1).toString()),
labelStyle: TextStyles.textXSSemibold.copyWith(
color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
),
labelStyle: TextStyles.textXSSemibold.copyWith(color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600),
selected: selectedIndexes[dayNumber] == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
},
);
}
@@ -588,9 +523,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MainBackgroundBouble(
padding: const EdgeInsets.only(
top: 16,
),
padding: const EdgeInsets.only(top: 16),
child: Column(
children: [
Padding(
@@ -598,31 +531,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${s.day} ${dayNumber + 1}',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: CustomDivider(),
),
Text(
s.meal,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()),
Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
choiceChipWidget(0),
choiceChipWidget(1),
choiceChipWidget(2),
choiceChipWidget(3),
choiceChipWidget(4),
],
children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)],
),
const SizedBox(height: 8),
ListView.builder(
@@ -655,20 +569,10 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
daily.name,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const Spacer(),
Text(
'${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}',
style: TextStyles.textMSemibold.copyWith(
color: appTheme.black,
),
),
Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
],
),
);

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -64,9 +62,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
void _scrollToBottom() {
_jumped = true;
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
@override
@@ -75,17 +71,8 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
return PopScope(
child: MultiBlocProvider(
providers: [
BlocProvider<CommentsCubit>(
create: (context) => commentsCubit
..getComments(
widget.slug,
CommentSourceType.plan,
true,
),
),
BlocProvider<AboutPlanCubit>(
create: (context) => sl<AboutPlanCubit>()..getPlanWithLoading(widget.slug),
),
BlocProvider<CommentsCubit>(create: (context) => commentsCubit..getComments(widget.slug, CommentSourceType.plan, true)),
BlocProvider<AboutPlanCubit>(create: (context) => sl<AboutPlanCubit>()..getPlanWithLoading(widget.slug)),
],
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState commentsState) {
@@ -166,14 +153,13 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.black.withValues(alpha: 0.1),
),
),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -189,9 +175,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: appTheme.greenA700,
onPressed: () async {
final (chosenPlan, error) = await context.read<AboutPlanCubit>().chosePlan();
@@ -205,18 +189,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
return;
}
if (!context.mounted) return;
GoRouter.of(context).pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chosenPlan.chatId,
'sendInitialMessage': true,
},
);
GoRouter.of(context).pushReplacementNamed('chatScreen', extra: {'uuid': chosenPlan.chatId, 'sendInitialMessage': true});
},
label: Text(
s.to_order,
style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
label: Text(s.to_order, style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700)),
),
),
),
@@ -255,17 +230,10 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(
Icons.close_rounded,
size: 18,
color: Colors.white,
),
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
@@ -281,22 +249,12 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false, // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -345,11 +303,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
),
],
@@ -389,7 +343,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -436,8 +392,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
'assets/icons/heart.svg',
width: 14,
height: 12,
colorFilter:
state.planModel?.wasLikedByMe == true ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
colorFilter: state.planModel?.wasLikedByMe == true
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: null,
),
const SizedBox(width: 8),
Text(
@@ -467,24 +424,16 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
s.about_plan,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
child: Text(s.about_plan, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(plan: state.planModel, type: ProductType.plan),
ExpandableText(
text: state.planModel?.details ?? '',
),
ExpandableText(text: state.planModel?.details ?? ''),
if (state.planModel != null) ...[
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: (state.planModel?.author)!,
title: s.plan_author,
),
child: AuthorCard(author: (state.planModel?.author)!, title: s.plan_author),
),
],
const SizedBox(height: 16),
@@ -503,22 +452,11 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
const SizedBox(height: 12),
],
switch (state.chosenOptionIndex) {
1 => OptionWidget(
option: state.planModel?.packet2Options,
duration: state.planModel?.duration ?? 0,
),
2 => OptionWidget(
option: state.planModel?.packet3Options,
duration: state.planModel?.duration ?? 0,
),
_ => OptionWidget(
option: state.planModel?.packet1Options,
duration: state.planModel?.duration ?? 0,
),
1 => OptionWidget(option: state.planModel?.packet2Options, duration: state.planModel?.duration ?? 0),
2 => OptionWidget(option: state.planModel?.packet3Options, duration: state.planModel?.duration ?? 0),
_ => OptionWidget(option: state.planModel?.packet1Options, duration: state.planModel?.duration ?? 0),
},
const SizedBox(
height: 16,
),
const SizedBox(height: 16),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
@@ -528,9 +466,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
alignment: Alignment.centerLeft,
child: Text(
'${state.planModel?.totalComments ?? 0} ${getCommentForm(state.planModel?.totalComments ?? 0, s)}',
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray500,
),
style: TextStyles.textMMedium.copyWith(color: appTheme.gray500),
),
),
const SizedBox(height: 16),

View File

@@ -1,6 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -100,8 +98,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
@@ -235,7 +239,9 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -387,7 +393,11 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(
id: recipeState.recipe.slug, commentSourceType: CommentSourceType.recipe, comment: state.comment, replyId: replyId),
id: recipeState.recipe.slug,
commentSourceType: CommentSourceType.recipe,
comment: state.comment,
replyId: replyId,
),
],
),
),

View File

@@ -65,10 +65,16 @@ class _GeneralInfoScreenState extends State<GeneralInfoScreen> {
appBar: AppBar(
title: const Text('Общая информация'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -45,9 +45,15 @@ class _OtherInfoScreenState extends State<OtherInfoScreen> {
appBar: AppBar(
title: const Text('Прочая информаиця'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -75,10 +75,16 @@ class _PhysicalActivityScreenState extends State<PhysicalActivityScreen> {
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text('Физическая нагрузка')),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 16, bottom: 80),
padding: const EdgeInsets.only(top: 16, bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
children: [
Row(

View File

@@ -45,10 +45,16 @@ class _PhysicalFormScreenState extends State<PhysicalFormScreen> {
appBar: AppBar(
title: const Text('Физическая форма'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -50,10 +50,16 @@ class _PressureScreenState extends State<PressureScreen> {
appBar: AppBar(
title: const Text('Давление'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.3.8+88
version: 1.3.10+90
publish_to: 'none'
environment:

View File

@@ -146,6 +146,8 @@ void main() {

View File

@@ -81,6 +81,8 @@ void main() {

View File

@@ -95,6 +95,8 @@ void main() {

View File

@@ -44,6 +44,8 @@ void main() {

View File

@@ -40,6 +40,8 @@ void main() {

View File

@@ -37,6 +37,8 @@ void main() {

View File

@@ -41,6 +41,8 @@ void main() {

View File

@@ -120,6 +120,8 @@ void main() {

View File

@@ -44,6 +44,8 @@ void main() {

View File

@@ -38,6 +38,8 @@ void main() {

View File

@@ -74,6 +74,8 @@ void main() {

View File

@@ -126,6 +126,8 @@ void main() {

View File

@@ -63,6 +63,8 @@ void main() {

View File

@@ -145,6 +145,8 @@ void main() {

View File

@@ -106,6 +106,8 @@ void main() {

View File

@@ -174,6 +174,8 @@ void main() {

View File

@@ -67,6 +67,8 @@ void main() {

View File

@@ -52,6 +52,8 @@ void main() {

View File

@@ -106,6 +106,8 @@ void main() {

View File

@@ -86,6 +86,8 @@ void main() {

View File

@@ -75,6 +75,8 @@ void main() {

View File

@@ -81,6 +81,8 @@ void main() {

View File

@@ -68,6 +68,8 @@ void main() {

View File

@@ -100,6 +100,8 @@ void main() {

View File

@@ -118,6 +118,8 @@ void main() {

View File

@@ -148,6 +148,8 @@ void main() {

View File

@@ -60,6 +60,8 @@ void main() {

View File

@@ -61,6 +61,8 @@ void main() {

View File

@@ -73,3 +73,5 @@ void main() {