From 0208ed753a15fa72170e3e4a59771d81680538ef Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 24 Aug 2026 11:38:01 +0300 Subject: [PATCH] feat(image_validation): image validation fix --- .../blog_post_create_cubit.dart | 11 +++ .../pages/blog_post_create_screen.dart | 77 ++++++++++++++++--- lib/generated/intl/messages_en.dart | 3 + lib/generated/intl/messages_ru.dart | 3 + lib/generated/l10n.dart | 10 +++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ru.arb | 3 +- 7 files changed, 98 insertions(+), 10 deletions(-) diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index 33a584ac..660d1b10 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -37,6 +37,17 @@ class BlogPostCreateCubit extends Cubit { } } + void setImagesError(String? error) { + if (state.imagesError == error && + state.formError == null && + state.errorMessage == null) { + return; + } + emit( + state.copyWith(imagesError: error, formError: null, errorMessage: null), + ); + } + void resetFormErrors() { if (!state.hasValidationErrors && !state.isSuccess && diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index b3c9b42c..432117dd 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_router.dart'; @@ -38,11 +39,15 @@ class BlogPostCreateScreen extends StatefulWidget { } class _BlogPostCreateScreenState extends State { + static const _minImageWidth = 500; + static const _minImageHeight = 500; + final blogPostCubit = sl(); bool shrinkImage = false; late StreamSubscription keyboardSubscription; final List savedImages = []; final List pendingImages = []; + final Set _undersizedPendingPaths = {}; final List mediaIdsToDelete = []; final titleController = TextEditingController(); final descriptionController = TextEditingController(); @@ -531,10 +536,7 @@ class _BlogPostCreateScreenState extends State { source: ImageSource.camera, ); if (image != null && _remainingSlots > 0) { - setState(() { - pendingImages.add(image); - }); - blogPostCubit.clearImagesError(); + await _addPendingImages([image]); } AppRouter.navigatorKey.currentContext?.pop(); }, @@ -561,10 +563,9 @@ class _BlogPostCreateScreenState extends State { limit: _remainingSlots, ); if (images.isNotEmpty) { - setState(() { - pendingImages.addAll(images.take(_remainingSlots)); - }); - blogPostCubit.clearImagesError(); + await _addPendingImages( + images.take(_remainingSlots).toList(), + ); } if (!context.mounted) return; AppRouter.navigatorKey.currentContext?.pop(); @@ -603,9 +604,12 @@ class _BlogPostCreateScreenState extends State { } void _removePendingImage(int index) { + final path = pendingImages[index].path; setState(() { pendingImages.removeAt(index); + _undersizedPendingPaths.remove(path); }); + _syncImagesSizeError(); } void _onReorderGallery(int oldIndex, int newIndex) { @@ -839,6 +843,11 @@ class _BlogPostCreateScreenState extends State { return; } + if (_undersizedPendingPaths.isNotEmpty) { + blogPostCubit.setImagesError(S.current.blog_post_image_min_size_error); + return; + } + final title = titleController.text.trim(); final description = descriptionController.text.trim(); final videoUrl = videoUrlController.text.trim(); @@ -907,6 +916,50 @@ class _BlogPostCreateScreenState extends State { ); } + Future _addPendingImages(List images) async { + if (images.isEmpty) { + return; + } + + final undersizedPaths = {}; + for (final image in images) { + if (!await _isImageLargeEnough(image)) { + undersizedPaths.add(image.path); + } + } + + setState(() { + pendingImages.addAll(images); + _undersizedPendingPaths.addAll(undersizedPaths); + }); + _syncImagesSizeError(); + } + + void _syncImagesSizeError() { + final minSizeError = S.current.blog_post_image_min_size_error; + if (_undersizedPendingPaths.isNotEmpty) { + blogPostCubit.setImagesError(minSizeError); + return; + } + if (blogPostCubit.state.imagesError == minSizeError) { + blogPostCubit.clearImagesError(); + } + } + + Future _isImageLargeEnough(XFile image) async { + try { + final bytes = await File(image.path).readAsBytes(); + final descriptor = await ui.ImmutableBuffer.fromUint8List(bytes); + final codec = await ui.instantiateImageCodecFromBuffer(descriptor); + final frame = await codec.getNextFrame(); + final width = frame.image.width; + final height = frame.image.height; + return width >= _minImageWidth && height >= _minImageHeight; + } catch (_) { + return false; + } + } + Widget _buildGallery() { final itemHeight = shrinkImage ? 72.0 : 112.0; @@ -946,6 +999,7 @@ class _BlogPostCreateScreenState extends State { child: _buildGalleryTile( index: index, height: itemHeight, + hasError: _undersizedPendingPaths.contains(pending.path), imageProvider: FileImage(File(pending.path)), onRemove: () => _removePendingImage(pendingIndex), ), @@ -985,6 +1039,7 @@ class _BlogPostCreateScreenState extends State { required double height, required ImageProvider imageProvider, required VoidCallback onRemove, + bool hasError = false, }) { return ReorderableDelayedDragStartListener( index: index, @@ -992,12 +1047,16 @@ class _BlogPostCreateScreenState extends State { width: height, height: height, margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: hasError ? Border.all(color: appTheme.red, width: 2) : null, + ), child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: ClipRRect( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(14), child: Image(image: imageProvider, fit: BoxFit.cover), ), ), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 81d5acf0..8929ab56 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -137,6 +137,9 @@ class MessageLookup extends MessageLookupByLibrary { "Средние показатели давления:", ), "back_to_chat": MessageLookupByLibrary.simpleMessage("Вернуться в чат"), + "blog_post_image_min_size_error": MessageLookupByLibrary.simpleMessage( + "Минимальный размер изображения — 500×500 px", + ), "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), "by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"), "calls_for_bullying": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index a1c9f2c3..91ba0fac 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -55,6 +55,9 @@ class MessageLookup extends MessageLookupByLibrary { "average_pressure": MessageLookupByLibrary.simpleMessage( "Средние показатели давления:", ), + "blog_post_image_min_size_error": MessageLookupByLibrary.simpleMessage( + "Минимальный размер изображения — 500×500 px", + ), "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), "by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"), "calories": MessageLookupByLibrary.simpleMessage("Калории"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index d5c739d2..f6d24a07 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2569,6 +2569,16 @@ class S { ); } + /// `Минимальный размер изображения — 500×500 px` + String get blog_post_image_min_size_error { + return Intl.message( + 'Минимальный размер изображения — 500×500 px', + name: 'blog_post_image_min_size_error', + desc: '', + args: [], + ); + } + /// `Информация о заказе` String get infoAboutOrder { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2c971d99..bd2406d8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -317,6 +317,7 @@ "could_not_publish_blog_post": "Не удалось опубликовать пост", "could_not_update_blog_post": "Не удалось обновить пост", "could_not_delete_blog_post": "Не удалось удалить пост", + "blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px", "infoAboutOrder": "Информация о заказе", "to_order": "Заказать", "can_not_choose_start_date": "Дату старта плана невозможно выбрать, пока Вы не привяжите карту", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 738bac08..5d729fe6 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -270,5 +270,6 @@ "video_call_close": "Закрыть", "video_call_notification_in_progress": "Звонок идёт", "video_call_notification_return": "Вернуться к звонку", - "video_call_notification_end": "Завершить звонок" + "video_call_notification_end": "Завершить звонок", + "blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px" }