diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 70ff32b8..75372b09 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.32.0" + flutter_version: "3.32.5" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/android/app/build.gradle b/android/app/build.gradle index 4df5da7f..a5377cc6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion - ndkVersion = "27.0.12077973" + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_21 diff --git a/assets/icons/blocked_users.svg b/assets/icons/blocked_users.svg new file mode 100644 index 00000000..819b2245 --- /dev/null +++ b/assets/icons/blocked_users.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7c47de93..648ad30f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 37; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 37; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 37; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4c5b3c2d..9284d469 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 32 + 37 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 746303b4..70107e0a 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -25,6 +25,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; @@ -193,6 +194,11 @@ class AppRouter { ), ], ), + GoRoute( + name: 'blockedUsersScreen', + path: '/blocked-users-screen', + builder: (context, state) => const BlockedUsersScreen(), + ), GoRoute( path: '/welcome-screen', name: 'welcomeScreen', diff --git a/lib/core/image_compressing/image_compress_utility.dart b/lib/core/image_compressing/image_compress_utility.dart index 6eb66f4e..e3c5fa65 100644 --- a/lib/core/image_compressing/image_compress_utility.dart +++ b/lib/core/image_compressing/image_compress_utility.dart @@ -7,11 +7,25 @@ abstract class ImageCompressUtility { static Future> compressFilesList(List files) async { final List compressedFilesList = []; for (int i = 0; i < files.length; i++) { - compressedFilesList.add(await compressFile(files[i])); + if (_isImageFile(files[i].path)) { + compressedFilesList.add(await compressFile(files[i])); + } else { + compressedFilesList.add(files[i]); + } } return compressedFilesList; } + static bool _isImageFile(String path) { + final lower = path.toLowerCase(); + return lower.endsWith('.png') || + lower.endsWith('.jpg') || + lower.endsWith('.jpeg') || + lower.endsWith('.gif') || + lower.endsWith('.bmp') || + lower.endsWith('.webp'); + } + static Future compressFile(File file) async { final image = img.decodeImage(file.readAsBytesSync())!; int width = image.width; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 0b83e3d7..9036809a 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -37,6 +37,9 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; @@ -164,6 +167,7 @@ Future init() async { sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => BlogPostCubit(sl())); + sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -193,6 +197,7 @@ Future init() async { sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); + sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -224,6 +229,7 @@ Future init() async { sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => FirebaseApi()); sl.registerLazySingleton(() => ResetPasswordService(sl())); + sl.registerLazySingleton(() => BlockedUserService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart index f10ae9ec..bcc82aeb 100644 --- a/lib/core/utils/date_time_utils.dart +++ b/lib/core/utils/date_time_utils.dart @@ -33,7 +33,7 @@ extension DateTimeExtension on DateTime { } } -String calculateDifference(DateTime dateTime) { +String calculateDifference(DateTime dateTime, [String? format]) { final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime; if (localDateTime.isToday()) { @@ -41,6 +41,6 @@ String calculateDifference(DateTime dateTime) { } else if (localDateTime.isYesterday()) { return 'Вчера'; } else { - return localDateTime.format('d MMMM', 'ru'); + return localDateTime.format(format ?? 'd MMMM', 'ru'); } } diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 152f0fb9..73431542 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -1,9 +1,12 @@ import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; +import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; class WsManager { final WsService wsService; @@ -71,6 +74,11 @@ class WsManager { } }); client?.eventStream.listen((event) { + EasyDebounce.debounce( + 'searchChatRoom', + const Duration(milliseconds: 300), + () => sl().getChatRooms(), + ); log(event.rootObject.toString()); }); _subscribe(); diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index dbe04ec4..1ba0224e 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,7 @@ class CustomPopup extends StatelessWidget { final FeedBlogPostModel? feedBlogPostModel; final Recipe? recipe; final Diet? diet; + final String userSlug; const CustomPopup({ super.key, @@ -39,6 +41,7 @@ class CustomPopup extends StatelessWidget { this.feedBlogPostModel, this.recipe, this.diet, + required this.userSlug }); @override @@ -164,6 +167,13 @@ class CustomPopup extends StatelessWidget { ); } }); + addItem( + 'assets/icons/complain.svg', + 'Заблокировать автора', + () { + sl().blockUser(userSlug); + }, + ); } if (entries.isNotEmpty && entries.last is PopupMenuItem) { diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 1dc5bd36..5541daa8 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -195,7 +195,8 @@ class _BlogPostScreenState extends State { path: 'blog_post/${(widget.id)}', id: widget.id, feedBlogPostModel: blogPostState.blogPostModel, - ), + userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '', + ), const SizedBox(width: 16), ], ), diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 369993e0..f36cbbe5 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -7,11 +7,13 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; @@ -167,6 +169,11 @@ class ChatRoomCubit extends Cubit { List messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; chatRepository.emitNewMessageToChats(message); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); + EasyDebounce.debounce( + 'searchChatRoom', + const Duration(milliseconds: 300), + () => sl().getChatRooms(), + ); }); typingSubscription = wsManager.clientTypingStream?.listen((event) { @@ -184,7 +191,7 @@ class ChatRoomCubit extends Cubit { try { await chatRepository.sendMessage( data: data, - files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, + files: files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null, slug: userSlug, ); AudioPlayer().play((AssetSource('audio/send_message.wav'))); diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index e2e222cd..ab1c80c5 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:dismissible_page/dismissible_page.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; @@ -8,6 +9,7 @@ import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -47,6 +49,7 @@ class _ChatScreenState extends State { final List<({int id, int index})> unreadMessagesIdsCopy = []; final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates(); late AppLifecycleListener _listener; + List pickedFiles = []; void _onHide() { chatRoomCubit.cancelTimer(); @@ -336,73 +339,155 @@ class _ChatScreenState extends State { ), Container( padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 8), - InkWell( - onTap: () => showImagePicker(state.chatRoom.users.receiver.slug), - customBorder: const CircleBorder(), - child: Ink( - height: 40, - width: 40, - child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (int i = 0; i < pickedFiles.length; i++) ...[ + Stack( + clipBehavior: Clip.none, + children: [ + _isImageFile(pickedFiles[i].path) + ? GestureDetector( + onTap: () { + final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList(); + final imageIndex = _imageIndexAt(pickedFiles, i); + + context.pushTransparentRoute( + PhotoViewScreen( + imageUrls: imageFiles.map((e) => e.path).toList(), + isLocal: true, + index: imageIndex, + ), + ); + }, + child: Hero( + tag: pickedFiles[i].path, + transitionOnUserGestures: true, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + height: 104, + width: 104, + child: Image.file( + pickedFiles[i], + fit: BoxFit.cover, + ), + ), + ), + ), + ) + : _buildFilePreview(pickedFiles[i]), + Positioned( + top: -8, + right: -8, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + pickedFiles.remove(pickedFiles[i]); + }); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: appTheme.gray50, + shape: BoxShape.circle, + ), + child: const Icon( + size: 20, + Icons.close, + color: Colors.black, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 8), + ], + ], ), ), - 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), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const SizedBox(width: 8), + InkWell( + onTap: () => showImagePicker(state.chatRoom.users.receiver.slug), + customBorder: const CircleBorder(), + child: Ink( + height: 40, + width: 40, + child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), + ), + ), + 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), + ), + ), ), ), ), - ), + const SizedBox(width: 8), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) { + chatRoomCubit.sendMessage( + data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(), + files: pickedFiles.isEmpty ? null : pickedFiles, + userSlug: state.chatRoom.users.receiver.slug, + ); + setState(() { + textEditingController.clear(); + pickedFiles.clear(); + }); + } + }, + child: Ink( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + height: 40, + width: 40, + child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white), + ), + ), + const SizedBox(width: 8), + ], ), - const SizedBox(width: 8), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - if (textEditingController.text.isNotEmpty) { - chatRoomCubit.sendMessage( - data: textEditingController.text.trim(), - userSlug: state.chatRoom.users.receiver.slug, - ); - setState(() { - textEditingController.clear(); - }); - } - }, - child: Ink( - decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), - height: 40, - width: 40, - child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white), - ), - ), - const SizedBox(width: 8), ], ), ), @@ -425,6 +510,56 @@ class _ChatScreenState extends State { ); } + Widget _buildFilePreview(File file) { + final fileName = file.path.split('/').last; + + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + height: 104, + width: 104, + color: const Color(0xFFE0E0E0), // Светло-серый фон + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.attach_file, size: 32, color: Colors.black54), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + fileName, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ), + ], + ), + ), + ); + } + + bool _isImageFile(String path) { + final lower = path.toLowerCase(); + return lower.endsWith('.png') || + lower.endsWith('.jpg') || + lower.endsWith('.jpeg') || + lower.endsWith('.gif') || + lower.endsWith('.bmp') || + lower.endsWith('.webp'); + } + + int _imageIndexAt(List allFiles, int targetIndex) { + int count = 0; + for (int i = 0; i < allFiles.length; i++) { + if (_isImageFile(allFiles[i].path)) { + if (i == targetIndex) return count; + count++; + } + } + return 0; + } + showImagePicker(String slug) { showCupertinoModalPopup( context: context, @@ -435,9 +570,14 @@ class _ChatScreenState extends State { Navigator.of(context).pop(); final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); - if (image != null) { - await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); - } + setState(() { + if (image != null) { + pickedFiles.add(File(image.path)); + } + }); + // if (image != null) { + // await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); + // } }, child: const Row( children: [ @@ -453,8 +593,11 @@ class _ChatScreenState extends State { final picker = ImagePicker(); List images = await picker.pickMultiImage(); if (images != []) { - List files = images.map((image) => File(image.path)).toList(); - await chatRoomCubit.sendMessage(files: files, userSlug: slug); + setState(() { + pickedFiles.addAll(images.map((image) => File(image.path)).toList()); + }); + // List files = images.map((image) => File(image.path)).toList(); + // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, child: const Row( @@ -493,8 +636,11 @@ class _ChatScreenState extends State { ], ); if (result != null) { - List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage(files: files, userSlug: slug); + // List files = result.paths.map((path) => File(path!)).toList(); + setState(() { + pickedFiles.addAll(result.paths.map((path) => File(path!)).toList()); + }); + // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, child: const Row( diff --git a/lib/features/chat/presentation/pages/chats_screen.dart b/lib/features/chat/presentation/pages/chats_screen.dart index 03c3870a..6c4c2398 100644 --- a/lib/features/chat/presentation/pages/chats_screen.dart +++ b/lib/features/chat/presentation/pages/chats_screen.dart @@ -67,11 +67,16 @@ class _ChatsScreenState extends State { ), ]; }, - body: switch (state.state) { - BaseState.error => DataErrorWidget(onReload: () => context.read().getChatRooms()), - BaseState.success => _buildChatList(context, state), - _ => const Loader(), - }, + body: RefreshIndicator( + child: switch (state.state) { + BaseState.error => DataErrorWidget(onReload: () => context.read().getChatRooms()), + BaseState.success => _buildChatList(context, state), + _ => const Loader(), + }, + onRefresh: () async { + await context.read().getChatRooms(); + }, + ), ), ); }, diff --git a/lib/features/chat/presentation/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/widgets/chat_entry_widget.dart index 1338c60b..44cc67f9 100644 --- a/lib/features/chat/presentation/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_entry_widget.dart @@ -20,15 +20,14 @@ class ChatEntryWidget extends StatefulWidget { class _ChatEntryWidgetState extends State { @override Widget build(BuildContext context) { - return InkWell( - borderRadius: BorderRadius.circular(16), + return GestureDetector( onTap: () => GoRouter.of(context).pushNamed( 'chatScreen', extra: { 'chatId': widget.chatRoom.id.toString(), }, ), - child: Ink( + child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), @@ -61,7 +60,13 @@ class _ChatEntryWidgetState extends State { SizedBox( width: MediaQuery.sizeOf(context).width * 0.5, child: Text( - widget.chatRoom.lastMessage!.data.toString().trim(), + widget.chatRoom.lastMessage!.mediaData != null + ? widget.chatRoom.lastMessage!.isCurrentUserAuthor == true + ? 'Вы: 📎 Вложение' + : '📎 Вложение' + : widget.chatRoom.lastMessage!.isCurrentUserAuthor == true + ? 'Вы: ${widget.chatRoom.lastMessage!.data.toString().trim()}' + : widget.chatRoom.lastMessage!.data.toString().trim(), maxLines: 1, overflow: TextOverflow.ellipsis, style: CustomTextStyles.bodyMediumBluegray800, @@ -77,7 +82,7 @@ class _ChatEntryWidgetState extends State { Padding( padding: const EdgeInsets.only(left: 8), child: Text( - widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'), + calculateDifference(widget.chatRoom.lastMessage!.updatedAt, 'dd MMM'), style: theme.textTheme.bodyMedium, ), ), diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index e249c708..54924fcd 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; @@ -51,7 +52,8 @@ class ChatRepository { data: data, mediaData: multipartFiles.isEmpty ? null : multipartFiles, ); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); rethrow; } } diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 3901841f..fa88e9d8 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -194,6 +194,7 @@ class _DietPreviewScreenState extends State { isAuthor: widget.diet.author.authorIsMe, path: 'diet/${widget.diet.slug}', complainedContentType: ComplainedContent.diet, + userSlug: widget.diet.author.authorSlug, ), const SizedBox(width: 16), ], diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index a57e7580..eab3c07e 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -51,6 +51,29 @@ class _DevToolsScreenContentState extends State { GetPlansComments(), SendPlansComment(), GetRecipesComments(), + AuthServiceTest(), + DiaryServiceTest(), + BlockedUserServiceTest(), + PlanServiceTest(), + AboutRecipeServiceTest(), + BlogPostServiceTest(), + CalendarServiceTest(), + CardsServiceTest(), + ConsultantServiceTest(), + ConsultationServiceTest(), + CreateDietServiceTest(), + CreatePlanServiceTest(), + GlossaryServiceTest(), + MyDietsServiceTest(), + MyPlansServiceTest(), + MyRecipesServiceTest(), + NotificationsServiceTest(), + PlansServiceTest(), + ProfileServiceTest(), + RegistrationServiceTest(), + ResetPasswordServiceTest(), + SettingsServiceTest(), + WsServiceTest(), ]; final controller = TextEditingController(); late final List testsStates; @@ -132,17 +155,56 @@ class _DevToolsScreenContentState extends State { setState(() { testsStates[index] = TestState.loading; }); - final result = await testCases[index].run(baseUrl: baseUrlForTest); - if (result) { - setState(() { - testsStates[index] = TestState.success; - }); - showSuccessToast('${testCases[index].name} passed'); - } else { + try { + final result = await testCases[index].run(baseUrl: baseUrlForTest); + if (result.success) { + setState(() { + testsStates[index] = TestState.success; + }); + showSuccessToast('${testCases[index].name} passed'); + } else { + setState(() { + testsStates[index] = TestState.fail; + }); + showErrorToast('${testCases[index].name} failed'); + // Show AlertDialog with error description + if (mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Test Failed'), + content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + } catch (e, stack) { setState(() { testsStates[index] = TestState.fail; }); showErrorToast('${testCases[index].name} failed'); + // Show AlertDialog with error description + if (mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Test Failed'), + content: SingleChildScrollView(child: Text(e.toString() + '\n' + stack.toString())), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } } }, buttonStyle: const ButtonStyle(), diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index 1b2f8961..59f92ce3 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -4,19 +4,48 @@ import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; +import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; +import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; +import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; +import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.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/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; +import 'package:eatvio_app/features/plans/services/plans_service.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart'; import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/diary/services/diary_service.dart'; +import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; +import 'package:eatvio_app/features/registration/services/registration_service.dart'; +import 'package:eatvio_app/features/settings/services/settings_service.dart'; + +class TestResult { + final bool success; + final String? error; + TestResult(this.success, [this.error]); +} sealed class TestCases { String get name => throw UnimplementedError(); - Future run({required String baseUrl}) async => throw UnimplementedError(); + Future run({required String baseUrl}) async => throw UnimplementedError(); } class CreateRecipeTask implements TestCases { @@ -24,7 +53,7 @@ class CreateRecipeTask implements TestCases { String get name => 'api/recipes/create'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl)); @@ -41,10 +70,16 @@ class CreateRecipeTask implements TestCases { steps: [Step(description: 'desc')], ), ); - } catch (e) { - return false; + } on DioError catch (e) { + final res = e.response; + if (res != null && (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300)) { + return TestResult(false, 'DioError: statusCode=${res.statusCode}\n${res.data}'); + } + return TestResult(false, e.toString()); + } catch (e, stack) { + return TestResult(false, e.toString() + '\n' + stack.toString()); } - return true; + return TestResult(true); } } @@ -53,82 +88,66 @@ class Test850task implements TestCases { String get name => 'task #850'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { - // get all of needed services final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final profileService = ProfileService(dio, baseUrl: baseUrl); final dietsService = DietService(dio, baseUrl: baseUrl); - - // get all of needed data final dietsPaginationModel = await feedService.getDiets(page: 1); final chosenDietsPaginationModel = await profileService.getChosenDiets(); final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull; final today = DateTime.now(); final yesterday = today.subtract(const Duration(days: 1)); final tomorrow = today.add(const Duration(days: 1)); - - // run test case for (final diet in dietsPaginationModel.data) { if (!diet.author.authorIsMe) { if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') { try { await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0); - } catch (e) { + } catch (e, stack) { log('unexpected error7: $e'); - return false; + return TestResult(false, e.toString() + '\n' + stack.toString()); } await Future.delayed(const Duration(seconds: 3)); } try { await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')}); - - return false; + return TestResult(false, 'Expected error but got success'); } catch (e) { log('expect ot have error: $e'); } - - // test start diet with correct date try { await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')}); - } catch (e) { + } catch (e, stack) { log('unexpected error2: $e'); - return false; + return TestResult(false, e.toString() + '\n' + stack.toString()); } - - // test cancelation of diet try { await dietsService.cancelDiet(id: diet.id); - } catch (e) { + } catch (e, stack) { log('unexpected error3: $e'); - return false; + return TestResult(false, e.toString() + '\n' + stack.toString()); } - await Future.delayed(const Duration(seconds: 3)); - - // test start diet with correct date try { await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')}); - } catch (e) { + } catch (e, stack) { log('unexpected error4: $e'); - return false; + return TestResult(false, e.toString() + '\n' + stack.toString()); } - - // test cancelation of diet try { await dietsService.cancelDiet(id: diet.id); - } catch (e) { + } catch (e, stack) { log('unexpected error5: $e'); } break; } } - - return true; - } catch (e) { + return TestResult(true); + } catch (e, stack) { log('unexpected error6: $e'); - return false; + return TestResult(false, e.toString() + '\n' + stack.toString()); } } } @@ -139,16 +158,16 @@ class GetChatRoomPreviews implements TestCases { String get name => '/api/chat/index'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { // get all of needed services // final dio = sl(); // final chatService = ChatService(dio, baseUrl: baseUrl); // await chatService.getChatRooms(); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -158,16 +177,16 @@ class GetChatRoom implements TestCases { String get name => '/api/chat/show/{id}'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { // final dio = sl(); // final chatService = ChatService(dio, baseUrl: baseUrl); // final chatRoomsModels = await chatService.getChatRoomPreviews(); // await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? ''); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -177,16 +196,16 @@ class SendMessage implements TestCases { String get name => '/api/chat/{id}/message/send'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { // final dio = sl(); // final chatService = ChatService(dio, baseUrl: baseUrl); // final chatRoomsModels = await chatService.getChatRoomPreviews(); // await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message'); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -196,16 +215,16 @@ class GetChatMessages implements TestCases { String get name => '/api/chat/show/{id}/messages?page={page}'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { // final dio = sl(); // final chatService = ChatService(dio, baseUrl: baseUrl); // final chatRoomsModels = await chatService.getChatRoomPreviews(); // await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -215,7 +234,7 @@ class GetChatRoomIdByUserSlug implements TestCases { String get name => '/api/chat/get_by_user/{user}'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final chatService = ChatService(dio, baseUrl: baseUrl); @@ -230,9 +249,9 @@ class GetChatRoomIdByUserSlug implements TestCases { break; } } - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -243,14 +262,14 @@ class GetBlogPostList implements TestCases { String get name => '/api/blogPosts/index'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); await feedService.getBlogPostList(page: 1); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -260,7 +279,7 @@ class GetRecipes implements TestCases { String get name => '/api/recipes/index'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); @@ -269,9 +288,9 @@ class GetRecipes implements TestCases { final tags = tagsModel.data; final tagsIds = tags.map((e) => e.id).toList(); await feedService.getRecipes(page: 1, tags: tagsIds, query: null); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -281,14 +300,14 @@ class GetDiets implements TestCases { String get name => '/api/diets/index'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); await feedService.getDiets(page: 1, query: null); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -298,14 +317,14 @@ class GetPlans implements TestCases { String get name => '/api/plans/index'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); await feedService.getPlans(page: 1, query: null); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -315,16 +334,16 @@ class GetBlogPostComments implements TestCases { String get name => '/api/blogPosts/{id}/comments/show'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); final blogPosts = blogPostsModel.feedBlogPostList; await feedService.getBlogPostComments(id: blogPosts.first.id.toString()); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -334,16 +353,16 @@ class SendBlogPostComment implements TestCases { String get name => '/api/blogPosts/{id}/comment/create'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); final blogPosts = blogPostsModel.feedBlogPostList; await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message'); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -353,16 +372,16 @@ class GetDietsComments implements TestCases { String get name => '/api/diets/{id}/comments/show'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final dietsModel = await feedService.getDiets(page: 1, query: null); final diets = dietsModel.data; await feedService.getDietsComments(id: diets.first.id.toString()); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -372,16 +391,16 @@ class SendDietsComment implements TestCases { String get name => '/api/diets/{id}/comment/create'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final dietsModel = await feedService.getDiets(page: 1, query: null); final diets = dietsModel.data; await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message'); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -391,16 +410,16 @@ class GetPlansComments implements TestCases { String get name => '/api/plans/{slug}/comments/show'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final plansModel = await feedService.getPlans(page: 1, query: null); final plans = plansModel.data; await feedService.getPlansComments(slug: plans.first.slug); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -410,16 +429,16 @@ class SendPlansComment implements TestCases { String get name => '/api/plans/{slug}/comment/create'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); final plansModel = await feedService.getPlans(page: 1, query: null); final plans = plansModel.data; await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message'); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); } } } @@ -429,7 +448,7 @@ class GetRecipesComments implements TestCases { String get name => '/api/recipes/{id}/comments/show'; @override - Future run({required String baseUrl}) async { + Future run({required String baseUrl}) async { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); @@ -440,9 +459,426 @@ class GetRecipesComments implements TestCases { final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null); final recipes = recipesModel.recipesList; await feedService.getRecipesComments(id: recipes.first.id.toString()); - return true; + return TestResult(true); } catch (e) { - return false; + return TestResult(false, e.toString()); + } + } +} + +// AuthService +class AuthServiceTest implements TestCases { + @override + String get name => 'AuthService: getTokenByCredentials'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final authService = AuthService(dio, baseUrl: baseUrl); + // Use dummy credentials, expecting failure but no crash + await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890')); + return TestResult(false, 'Should not succeed'); + } catch (e) { + return TestResult(true, 'Exception expected for dummy credentials'); + } + } +} + +// DiaryService +class DiaryServiceTest implements TestCases { + @override + String get name => 'DiaryService: getDiaryIndexByDate'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final diaryService = DiaryService(dio, baseUrl: baseUrl); + await diaryService.getDiaryIndexByDate(date: '2023-01-01'); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// BlockedUserService +class BlockedUserServiceTest implements TestCases { + @override + String get name => 'BlockedUserService: getBlockedUsers'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final blockedUserService = BlockedUserService(dio, baseUrl: baseUrl); + await blockedUserService.getBlockedUsers(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// PlanService +class PlanServiceTest implements TestCases { + @override + String get name => 'PlanService: getPlan'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final planService = PlanService(dio, baseUrl: baseUrl); + // Use a dummy slug, expecting failure but no crash + await planService.getPlan('dummy-slug'); + return TestResult(false, 'Should not succeed'); + } catch (e) { + return TestResult(true, 'Exception expected for dummy slug'); + } + } +} + +// AboutRecipeService +class AboutRecipeServiceTest implements TestCases { + @override + String get name => 'AboutRecipeService: getRecipe'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final aboutRecipeService = AboutRecipeService(dio, baseUrl: baseUrl); + await aboutRecipeService.getRecipe('dummy-id'); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for dummy id'); + } + } +} + +// BlogPostService +class BlogPostServiceTest implements TestCases { + @override + String get name => 'BlogPostService: createBlogPost'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final blogPostService = BlogPostService(dio, baseUrl: baseUrl); + // Use empty FormData, expecting failure but no crash + await blogPostService.createBlogPost(formData: FormData()); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for empty FormData'); + } + } +} + +// CalendarService +class CalendarServiceTest implements TestCases { + @override + String get name => 'CalendarService: getCalendar'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final calendarService = CalendarService(dio, baseUrl: baseUrl); + await calendarService.getCalendar(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// CardsService +class CardsServiceTest implements TestCases { + @override + String get name => 'CardsService: getCards'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final cardsService = CardsService(dio, baseUrl: baseUrl); + await cardsService.getCards(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// ConsultantService +class ConsultantServiceTest implements TestCases { + @override + String get name => 'ConsultantService: getMyRequestStatus'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final consultantService = ConsultantService(dio, baseUrl: baseUrl); + await consultantService.getMyRequestStatus(slug: 'dummy-slug'); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for dummy slug'); + } + } +} + +// ConsultationService +class ConsultationServiceTest implements TestCases { + @override + String get name => 'ConsultationService: getConsultations'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final consultationService = ConsultationService(dio); + await consultationService.getConsultations(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// CreateDietService +class CreateDietServiceTest implements TestCases { + @override + String get name => 'CreateDietService: getDietPortionsNutrition'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final createDietService = CreateDietService(dio, baseUrl: baseUrl); + await createDietService.getDietPortionsNutrition(diet: 0); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for diet: 0'); + } + } +} + +// CreatePlanService +class CreatePlanServiceTest implements TestCases { + @override + String get name => 'CreatePlanService: createPlan'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final createPlanService = CreatePlanService(dio, baseUrl: baseUrl); + await createPlanService.createPlan(formData: FormData()); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for empty FormData'); + } + } +} + +// GlossaryService +class GlossaryServiceTest implements TestCases { + @override + String get name => 'GlossaryService: getGlossaryByQuery'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final glossaryService = GlossaryService(dio, baseUrl: baseUrl); + await glossaryService.getGlossaryByQuery(query: 'test'); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// MyDietsService +class MyDietsServiceTest implements TestCases { + @override + String get name => 'MyDietsService: getDiets'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final myDietsService = MyDietsService(dio, baseUrl: baseUrl); + await myDietsService.getDiets(page: 1); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// MyPlansService +class MyPlansServiceTest implements TestCases { + @override + String get name => 'MyPlansService: getPlans'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final myPlansService = MyPlansService(dio, baseUrl: baseUrl); + await myPlansService.getPlans(page: 1); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// MyRecipesService +class MyRecipesServiceTest implements TestCases { + @override + String get name => 'MyRecipesService: getMyRecipes'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final myRecipesService = MyRecipesService(dio, baseUrl: baseUrl); + await myRecipesService.getMyRecipes(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// NotificationsService +class NotificationsServiceTest implements TestCases { + @override + String get name => 'NotificationsService: getNotifications'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final notificationsService = NotificationsService(dio); + await notificationsService.getNotifications(page: 1); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// PlansService +class PlansServiceTest implements TestCases { + @override + String get name => 'PlansService: getPlansBySlug'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final plansService = PlansService(dio, baseUrl: baseUrl); + await plansService.getPlansBySlug(author: 'dummy-author'); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for dummy author'); + } + } +} + +// ProfileService +class ProfileServiceTest implements TestCases { + @override + String get name => 'ProfileService: getMyProfile'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final profileService = ProfileService(dio, baseUrl: baseUrl); + await profileService.getMyProfile(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// RegistrationService +class RegistrationServiceTest implements TestCases { + @override + String get name => 'RegistrationService: getLocationsByAddress'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final registrationService = RegistrationService(dio, baseUrl: baseUrl); + await registrationService.getLocationsByAddress(query: 'test'); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// ResetPasswordService +class ResetPasswordServiceTest implements TestCases { + @override + String get name => 'ResetPasswordService: sendEmailCodeForPasswordChange'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final resetPasswordService = ResetPasswordService(dio, baseUrl: baseUrl); + await resetPasswordService.sendEmailCodeForPasswordChange(data: {'email': 'test@example.com'}); + return TestResult(false, 'Expected failure but got success'); + } catch (e) { + return TestResult(true, 'Exception expected for test@example.com'); + } + } +} + +// SettingsService +class SettingsServiceTest implements TestCases { + @override + String get name => 'SettingsService: getProfileSettings'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final settingsService = SettingsService(dio, baseUrl: baseUrl); + await settingsService.getProfileSettings(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); + } + } +} + +// WsService +class WsServiceTest implements TestCases { + @override + String get name => 'WsService: getWebsocket'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final wsService = WsService(dio, baseUrl: baseUrl); + await wsService.getWebsocket(); + return TestResult(true); + } catch (e) { + return TestResult(false, e.toString()); } } } diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 89fc1659..28d5292a 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -334,6 +334,7 @@ class _AboutDietScreenState extends State { complainedContentType: ComplainedContent.diet, id: dietState.diet.id, diet: dietState.diet, + userSlug: dietState.diet.author.authorSlug, ), const SizedBox(width: 16), ], diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 91edc060..d8bff7c9 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -130,6 +130,7 @@ class _BlogPostWidgetState extends State { path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}', id: state.feedBlogPostList[widget.index].id, feedBlogPostModel: state.feedBlogPostList[widget.index], + userSlug: state.feedBlogPostList[widget.index].author.authorSlug, ), ], ), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index fbd4730b..dcc80666 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -94,6 +94,7 @@ class _DietsWidgetState extends State { id: widget.diet.id, complainedContentType: ComplainedContent.diet, diet: widget.diet, + userSlug: widget.diet.author.authorSlug, ), ), ), diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 13b3b698..ea130f7e 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -100,6 +100,7 @@ class _PlanWidgetState extends State { id: widget.plan.id, slug: widget.plan.slug, complainedContentType: ComplainedContent.plan, + userSlug: widget.plan.author.authorSlug, ), ), ), diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index f9998f61..2e0ed35b 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -128,6 +128,7 @@ class _RecipesWidgetState extends State { slug: widget.recipe.slug, complainedContentType: ComplainedContent.recipe, recipe: widget.recipe, + userSlug: widget.recipe.author.authorSlug, ), ), ), diff --git a/lib/features/menu/models/blocked_users_model.dart b/lib/features/menu/models/blocked_users_model.dart new file mode 100644 index 00000000..a7abd7f6 --- /dev/null +++ b/lib/features/menu/models/blocked_users_model.dart @@ -0,0 +1,14 @@ +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'blocked_users_model.g.dart'; +part 'blocked_users_model.freezed.dart'; + +@freezed +abstract class BlockedUsersModel with _$BlockedUsersModel { + const factory BlockedUsersModel({ + @JsonKey(name: 'data') required List profilesList, + }) = _BlockedUsersModel; + + factory BlockedUsersModel.fromJson(Map json) => _$BlockedUsersModelFromJson(json); +} diff --git a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart new file mode 100644 index 00000000..bd2a156d --- /dev/null +++ b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart @@ -0,0 +1,29 @@ +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; +import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BlockedUsersCubit extends Cubit { + final BlockedUsersRepository blockedUsersRepository; + + BlockedUsersCubit({ + required this.blockedUsersRepository, + }) : super(const BlockedUsersState()); + + getBlockedUsers() async { + try { + emit(BlockedUsersState.loaded(profilesList: await blockedUsersRepository.getBlockedUsers())); + } catch (e, st) { + emit(BlockedUsersState.error(errorMessage: '$e $st')); + } + } + + blockUser(String slug) async { + await blockedUsersRepository.blockUserBySlug(slug); + await getBlockedUsers(); + } + + unblockUser(String slug) async { + await blockedUsersRepository.unblockUserBySlug(slug); + await getBlockedUsers(); + } +} diff --git a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart new file mode 100644 index 00000000..2d83cdc6 --- /dev/null +++ b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart @@ -0,0 +1,15 @@ +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'blocked_users_state.freezed.dart'; + +@freezed +abstract class BlockedUsersState with _$BlockedUsersState { + const factory BlockedUsersState() = _BlockedUsersState; + const factory BlockedUsersState.loading() = BlockedUsersStateLoading; + const factory BlockedUsersState.loaded({required List profilesList}) = BlockedUsersStateLoaded; + const factory BlockedUsersState.error({ + required String errorMessage, + }) = BlockedUsersStateError; +} diff --git a/lib/features/menu/presentation/pages/blocked_users_screen.dart b/lib/features/menu/presentation/pages/blocked_users_screen.dart new file mode 100644 index 00000000..2f632026 --- /dev/null +++ b/lib/features/menu/presentation/pages/blocked_users_screen.dart @@ -0,0 +1,43 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; +import 'package:eatvio_app/features/menu/presentation/widgets/blocked_user_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BlockedUsersScreen extends StatefulWidget { + const BlockedUsersScreen({super.key}); + + @override + State createState() => _BlockedUsersScreenState(); +} + +class _BlockedUsersScreenState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => sl()..getBlockedUsers(), + child: BlocBuilder( + builder: (BuildContext context, BlockedUsersState state) { + if (state is BlockedUsersStateLoaded) { + return Scaffold( + appBar: AppBar( + title: const Text('Чёрный список'), + ), + body: ListView.builder( + itemBuilder: (context, index) => BlockedUserWidget(profile: state.profilesList[index]), + itemCount: state.profilesList.length, + ), + ); + } else { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ), + ); + } +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index c8fa2135..0449113b 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -53,6 +53,8 @@ class MenuScreen extends StatelessWidget { _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), + _divider(context), + _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), // _divider(context), // _tile( // leading: ImageConstant.icnDevTools, diff --git a/lib/features/menu/presentation/repositories/blocked_users_repository.dart b/lib/features/menu/presentation/repositories/blocked_users_repository.dart new file mode 100644 index 00000000..96c072c4 --- /dev/null +++ b/lib/features/menu/presentation/repositories/blocked_users_repository.dart @@ -0,0 +1,47 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; + +class BlockedUsersRepository { + final BlockedUserService blockedUserService; + + BlockedUsersRepository({ + required this.blockedUserService, + }); + + Future> getBlockedUsers() async { + try { + return (await blockedUserService.getBlockedUsers()).profilesList; + } catch (e, st) { + log(e.toString(), stackTrace: st); + rethrow; + } + } + + blockUserBySlug(String slug) async { + try { + return await blockedUserService.blockUser( + data: { + 'slug': slug, + }, + ); + } catch (e, st) { + log(e.toString(), stackTrace: st); + rethrow; + } + } + + unblockUserBySlug(String slug) async { + try { + return await blockedUserService.unblockUser( + data: { + 'slug': slug, + }, + ); + } catch (e, st) { + log(e.toString(), stackTrace: st); + rethrow; + } + } +} diff --git a/lib/features/menu/presentation/widgets/blocked_user_widget.dart b/lib/features/menu/presentation/widgets/blocked_user_widget.dart new file mode 100644 index 00000000..494df21c --- /dev/null +++ b/lib/features/menu/presentation/widgets/blocked_user_widget.dart @@ -0,0 +1,83 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class BlockedUserWidget extends StatefulWidget { + final ProfileModel profile; + const BlockedUserWidget({super.key, required this.profile}); + + @override + State createState() => _BlockedUserWidgetState(); +} + +class _BlockedUserWidgetState extends State { + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => GoRouter.of(context).pushNamed( + 'profileScreen', + extra: Author( + authorSlug: widget.profile.slug, + authorName: widget.profile.name, + authorProfileIcon: widget.profile.profilePhotoUrl, + authorIsMe: false, + isEntitySubscribedByMe: false, + ), + ), + child: Ink( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + CircleAvatar( + foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + radius: 28, + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.profile.name, + style: const TextStyle( + color: Color(0xFF323F4B), + fontSize: 16, + fontWeight: FontWeight.w600, + height: 1, + ), + ), + ], + ), + const Spacer(), + InkWell( + onTap: () { + context.read().unblockUser(widget.profile.slug); + }, + borderRadius: BorderRadius.circular(4), + child: Ink( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF1F2F6), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'Убрать', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF6D6F71), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/menu/services/blocked_users_service.dart b/lib/features/menu/services/blocked_users_service.dart new file mode 100644 index 00000000..6a4ed987 --- /dev/null +++ b/lib/features/menu/services/blocked_users_service.dart @@ -0,0 +1,23 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/menu/models/blocked_users_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'blocked_users_service.g.dart'; + +@RestApi() +abstract class BlockedUserService { + factory BlockedUserService(Dio dio, {String baseUrl}) = _BlockedUserService; + + @GET('/api/blocked_users') + Future getBlockedUsers(); + + @POST('/api/blocked_users/block') + Future blockUser({ + @Body() required Map data, + }); + + @POST('/api/blocked_users/unblock') + Future unblockUser({ + @Body() required Map data, + }); +} diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 9dc4f880..d475d34b 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -14,6 +14,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart'; @@ -1175,6 +1176,8 @@ class _ProfileScreenState extends State { sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); + } else if (value == 'block') { + sl().blockUser(state.profile.slug); } else { showModalBottomSheet( context: context, @@ -1196,6 +1199,11 @@ class _ProfileScreenState extends State { value: 'complain', child: Text('Пожаловаться'), ), + if (widget.author != null) + const PopupMenuItem( + value: 'block', + child: Text('Заблокировать'), + ), ], child: const Icon(Icons.more_horiz_rounded), ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 0ad05b44..a4fc00cb 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -102,6 +102,7 @@ class _ProfileBlogPostWidgetState extends State { path: 'blog_post/${(state.blogPosts[widget.index].id)}', id: state.blogPosts[widget.index].id, feedBlogPostModel: state.blogPosts[widget.index], + userSlug: state.blogPosts[widget.index].author.authorSlug, ), ], ), diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index 2a7d489f..b9328ba3 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -228,6 +228,7 @@ class _AboutRecipeScreenState extends State { complainedContentType: ComplainedContent.recipe, slug: widget.recipeSlug, recipe: recipeState.recipe, + userSlug: recipeState.recipe.author.authorSlug, ), const SizedBox(width: 16), ], diff --git a/pubspec.yaml b/pubspec.yaml index 2a17b9dc..d11956ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.11+41 +version: 1.1.14+44 publish_to: 'none' environment: @@ -38,8 +38,8 @@ dependencies: easy_debounce: ^2.0.3 badges: ^3.1.2 google_fonts: ^6.2.1 - syncfusion_flutter_charts: ^29.2.5 - syncfusion_flutter_gauges: ^29.2.5 + syncfusion_flutter_charts: ^30.1.37 + syncfusion_flutter_gauges: ^30.1.37 table_calendar: ^3.2.0 flutter_keyboard_visibility: ^6.0.0 share_plus: ^11.0.0 @@ -83,7 +83,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true