forked from eatvio/eatvio_app
Merge pull request 'fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type' (#830) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#830
This commit is contained in:
@@ -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()));
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -25,3 +25,5 @@ abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmR
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ abstract class OAuthVerificationRequest with _$OAuthVerificationRequest {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestR
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 см')],
|
||||
],
|
||||
),
|
||||
|
||||
29
lib/features/barcode/models/barcode_product_model.dart
Normal file
29
lib/features/barcode/models/barcode_product_model.dart
Normal 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);
|
||||
}
|
||||
108
lib/features/barcode/repositories/barcode_repository.dart
Normal file
108
lib/features/barcode/repositories/barcode_repository.dart
Normal 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,
|
||||
}
|
||||
|
||||
17
lib/features/barcode/services/barcode_service.dart
Normal file
17
lib/features/barcode/services/barcode_service.dart
Normal 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});
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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('Использовать штрихкод'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -146,6 +146,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +81,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,6 +95,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,6 +174,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart' as chat_room;
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||
@@ -66,12 +67,16 @@ void main() {
|
||||
const slug = 'user-slug';
|
||||
const data = 'Test message';
|
||||
const socketId = 'socket-id-123';
|
||||
final messageFixture = FixtureReader.readAsMap('chat/chat_message_response.json');
|
||||
final expectedMessage = ChatMessageModel.fromJson(messageFixture);
|
||||
|
||||
// ignore: null_argument_to_non_null_type
|
||||
when(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).thenAnswer((_) async => Future.value());
|
||||
when(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).thenAnswer((_) async => expectedMessage);
|
||||
|
||||
await mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId);
|
||||
final result = await mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId);
|
||||
|
||||
expect(result, isA<ChatMessageModel>());
|
||||
expect(result.data, data);
|
||||
verify(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).called(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1038,6 +1038,8 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,5 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user