From 09e641ec632796e94662bd258aeb729bb22b28c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 12:56:28 +0300 Subject: [PATCH 01/12] feat(feed): add subscribe button --- .../feed/models/diets_pagination_model.dart | 1 + .../diets_pagination_model.freezed.dart | 53 ++++++++++++----- .../feed/models/diets_pagination_model.g.dart | 2 + .../widgets/blog_post_widget.dart | 24 ++------ .../widgets/subscribe_button_widget.dart | 57 +++++++++++++++++++ .../presentation/widgets/sub_widget.dart | 1 + 6 files changed, 105 insertions(+), 33 deletions(-) create mode 100644 lib/features/feed/presentation/widgets/subscribe_button_widget.dart diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index 6fe4f677..426306cd 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -46,6 +46,7 @@ class Author with _$Author { @JsonKey(name: "author_name") required String authorName, @JsonKey(name: "author_profile_icon") required String authorProfileIcon, @JsonKey(name: "author_is_me") required bool authorIsMe, + @JsonKey(name: "is_entity_subscribed_by_me") required bool isEntitySubscribedByMe, }) = _Author; factory Author.fromJson(Map json) => _$AuthorFromJson(json); diff --git a/lib/features/feed/models/diets_pagination_model.freezed.dart b/lib/features/feed/models/diets_pagination_model.freezed.dart index 38789091..34cd67d0 100644 --- a/lib/features/feed/models/diets_pagination_model.freezed.dart +++ b/lib/features/feed/models/diets_pagination_model.freezed.dart @@ -812,6 +812,8 @@ mixin _$Author { String get authorProfileIcon => throw _privateConstructorUsedError; @JsonKey(name: "author_is_me") bool get authorIsMe => throw _privateConstructorUsedError; + @JsonKey(name: "is_entity_subscribed_by_me") + bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; /// Serializes this Author to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -831,7 +833,9 @@ abstract class $AuthorCopyWith<$Res> { {@JsonKey(name: "author_slug") String authorSlug, @JsonKey(name: "author_name") String authorName, @JsonKey(name: "author_profile_icon") String authorProfileIcon, - @JsonKey(name: "author_is_me") bool authorIsMe}); + @JsonKey(name: "author_is_me") bool authorIsMe, + @JsonKey(name: "is_entity_subscribed_by_me") + bool isEntitySubscribedByMe}); } /// @nodoc @@ -853,6 +857,7 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author> Object? authorName = null, Object? authorProfileIcon = null, Object? authorIsMe = null, + Object? isEntitySubscribedByMe = null, }) { return _then(_value.copyWith( authorSlug: null == authorSlug @@ -871,6 +876,10 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author> ? _value.authorIsMe : authorIsMe // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -886,7 +895,9 @@ abstract class _$$AuthorImplCopyWith<$Res> implements $AuthorCopyWith<$Res> { {@JsonKey(name: "author_slug") String authorSlug, @JsonKey(name: "author_name") String authorName, @JsonKey(name: "author_profile_icon") String authorProfileIcon, - @JsonKey(name: "author_is_me") bool authorIsMe}); + @JsonKey(name: "author_is_me") bool authorIsMe, + @JsonKey(name: "is_entity_subscribed_by_me") + bool isEntitySubscribedByMe}); } /// @nodoc @@ -906,6 +917,7 @@ class __$$AuthorImplCopyWithImpl<$Res> Object? authorName = null, Object? authorProfileIcon = null, Object? authorIsMe = null, + Object? isEntitySubscribedByMe = null, }) { return _then(_$AuthorImpl( authorSlug: null == authorSlug @@ -924,6 +936,10 @@ class __$$AuthorImplCopyWithImpl<$Res> ? _value.authorIsMe : authorIsMe // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -935,7 +951,9 @@ class _$AuthorImpl implements _Author { {@JsonKey(name: "author_slug") required this.authorSlug, @JsonKey(name: "author_name") required this.authorName, @JsonKey(name: "author_profile_icon") required this.authorProfileIcon, - @JsonKey(name: "author_is_me") required this.authorIsMe}); + @JsonKey(name: "author_is_me") required this.authorIsMe, + @JsonKey(name: "is_entity_subscribed_by_me") + required this.isEntitySubscribedByMe}); factory _$AuthorImpl.fromJson(Map json) => _$$AuthorImplFromJson(json); @@ -952,10 +970,13 @@ class _$AuthorImpl implements _Author { @override @JsonKey(name: "author_is_me") final bool authorIsMe; + @override + @JsonKey(name: "is_entity_subscribed_by_me") + final bool isEntitySubscribedByMe; @override String toString() { - return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe)'; + return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe, isEntitySubscribedByMe: $isEntitySubscribedByMe)'; } @override @@ -970,13 +991,15 @@ class _$AuthorImpl implements _Author { (identical(other.authorProfileIcon, authorProfileIcon) || other.authorProfileIcon == authorProfileIcon) && (identical(other.authorIsMe, authorIsMe) || - other.authorIsMe == authorIsMe)); + other.authorIsMe == authorIsMe) && + (identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) || + other.isEntitySubscribedByMe == isEntitySubscribedByMe)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, authorSlug, authorName, authorProfileIcon, authorIsMe); + int get hashCode => Object.hash(runtimeType, authorSlug, authorName, + authorProfileIcon, authorIsMe, isEntitySubscribedByMe); /// Create a copy of Author /// with the given fields replaced by the non-null parameter values. @@ -996,12 +1019,13 @@ class _$AuthorImpl implements _Author { abstract class _Author implements Author { const factory _Author( - {@JsonKey(name: "author_slug") required final String authorSlug, - @JsonKey(name: "author_name") required final String authorName, - @JsonKey(name: "author_profile_icon") - required final String authorProfileIcon, - @JsonKey(name: "author_is_me") required final bool authorIsMe}) = - _$AuthorImpl; + {@JsonKey(name: "author_slug") required final String authorSlug, + @JsonKey(name: "author_name") required final String authorName, + @JsonKey(name: "author_profile_icon") + required final String authorProfileIcon, + @JsonKey(name: "author_is_me") required final bool authorIsMe, + @JsonKey(name: "is_entity_subscribed_by_me") + required final bool isEntitySubscribedByMe}) = _$AuthorImpl; factory _Author.fromJson(Map json) = _$AuthorImpl.fromJson; @@ -1017,6 +1041,9 @@ abstract class _Author implements Author { @override @JsonKey(name: "author_is_me") bool get authorIsMe; + @override + @JsonKey(name: "is_entity_subscribed_by_me") + bool get isEntitySubscribedByMe; /// Create a copy of Author /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/feed/models/diets_pagination_model.g.dart b/lib/features/feed/models/diets_pagination_model.g.dart index 61060256..c7e54383 100644 --- a/lib/features/feed/models/diets_pagination_model.g.dart +++ b/lib/features/feed/models/diets_pagination_model.g.dart @@ -74,6 +74,7 @@ _$AuthorImpl _$$AuthorImplFromJson(Map json) => _$AuthorImpl( authorName: json['author_name'] as String, authorProfileIcon: json['author_profile_icon'] as String, authorIsMe: json['author_is_me'] as bool, + isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool, ); Map _$$AuthorImplToJson(_$AuthorImpl instance) => @@ -82,6 +83,7 @@ Map _$$AuthorImplToJson(_$AuthorImpl instance) => 'author_name': instance.authorName, 'author_profile_icon': instance.authorProfileIcon, 'author_is_me': instance.authorIsMe, + 'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe, }; _$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index e4ad937c..45271667 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -7,11 +7,11 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; @@ -89,26 +89,10 @@ class _BlogPostWidgetState extends State { ], ), const Spacer(), - InkWell( - onTap: () { - Fluttertoast.showToast(msg: 'ща доделаем'); - }, - borderRadius: BorderRadius.circular(8), - child: Ink( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: appTheme.gray200, - ), - child: Text( - 'Подписаться', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), + if (!widget.feedBlogPostModel.author.isEntitySubscribedByMe && !widget.feedBlogPostModel.author.authorIsMe) + SubscribeButtonWidget( + slug: widget.feedBlogPostModel.author.authorSlug, ), - ), ], ), ), diff --git a/lib/features/feed/presentation/widgets/subscribe_button_widget.dart b/lib/features/feed/presentation/widgets/subscribe_button_widget.dart new file mode 100644 index 00000000..90074bef --- /dev/null +++ b/lib/features/feed/presentation/widgets/subscribe_button_widget.dart @@ -0,0 +1,57 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; +import 'package:flutter/material.dart'; + +class SubscribeButtonWidget extends StatefulWidget { + final String slug; + + const SubscribeButtonWidget({ + super.key, + required this.slug, + }); + + @override + State createState() => _SubscribeButtonWidgetState(); +} + +class _SubscribeButtonWidgetState extends State { + bool isSubscribe = false; + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: isSubscribe ? 0 : 1, + duration: Duration(milliseconds: 250), + child: InkWell( + onTap: () async { + try { + setState(() { + isSubscribe = true; + }); + await sl().subscribe(widget.slug); + } catch (e) { + setState(() { + isSubscribe = false; + }); + } + }, + borderRadius: BorderRadius.circular(8), + child: Ink( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: appTheme.gray200, + ), + child: Text( + 'Подписаться', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index 444c48f5..0a4e2d40 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -27,6 +27,7 @@ class _SubWidgetState extends State { authorName: widget.subModel.name, authorProfileIcon: widget.subModel.profilePhotoUrl, authorIsMe: false, + isEntitySubscribedByMe: false, ), ), child: Ink( From d3d41af2a62d4b1b40d6e6573fc01548c08fbc13 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 12:59:32 +0300 Subject: [PATCH 02/12] feat(feed): add time of post --- lib/features/feed/presentation/widgets/blog_post_widget.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 45271667..8d547f95 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -78,9 +78,7 @@ class _BlogPostWidgetState extends State { ), const SizedBox(height: 4), Text( - calculateDifference( - widget.feedBlogPostModel.createdAt, - ), + '${calculateDifference(widget.feedBlogPostModel.createdAt)} в ${widget.feedBlogPostModel.createdAt.hour.toString().padLeft(2, '0')}:${widget.feedBlogPostModel.createdAt.minute.toString().padLeft(2, '0')}', style: TextStyle( height: 1, color: Colors.black45, From 2b3ecf0bf5fd8611a38b1f1579a1057eb4b6989b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 14:50:11 +0300 Subject: [PATCH 03/12] feat(feed): edit and delete posts --- lib/core/app_router.dart | 9 +-- lib/core/locator_service.dart | 2 +- .../presentation/pages/blog_post_screen.dart | 43 +++++++++++++- .../blog_post_create_cubit.dart | 24 ++++++++ .../pages/blog_post_create_screen.dart | 57 ++++++++++++++----- .../repositories/blog_post_repository.dart | 57 +++++++++++++++++++ .../services/blog_post_service.dart | 12 ++++ .../services/blog_post_service.g.dart | 54 ++++++++++++++++++ .../presentation/cubit/create_diet_cubit.dart | 9 ++- .../bloc/feed_cubit/feed_cubit.dart | 2 +- .../feed/presentation/pages/feed_screen.dart | 7 ++- .../widgets/blog_post_widget.dart | 5 +- lib/main.dart | 2 + pubspec.yaml | 3 +- 14 files changed, 257 insertions(+), 29 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1cb3b901..6facfbb2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -19,7 +19,6 @@ import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.d import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; 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/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/pages/notifications_screen.dart'; @@ -167,13 +166,15 @@ class AppRouter { path: '/blog-post-create-screen', name: 'blogPostCreateScreen', builder: (context, state) => BlogPostCreateScreen( - feedCubit: state.extra != null ? state.extra as FeedCubit : null, + feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null, ), ), GoRoute( path: '/blog-post-screen', name: 'blogPostScreen', - builder: (context, state) => BlogPostScreen(blogPost: state.extra as FeedBlogPostModel), + builder: (context, state) => BlogPostScreen( + blogPost: state.extra as FeedBlogPostModel, + ), ), GoRoute( path: '/chat-screen', @@ -273,7 +274,7 @@ class AppRouter { path: DietPreviewScreen.path, name: DietPreviewScreen.name, builder: (context, state) { - final data = state.extra as Diet; + final data = state.extra as Diet; return DietPreviewScreen( diet: data, ); diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 8d6994f5..9f456ad2 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -60,11 +60,11 @@ Future init() async { sl.registerLazySingleton(() => InitializationCubit(chatRepository: sl())); sl.registerLazySingleton(() => NewMessagesCubit()); sl.registerLazySingleton(() => NotificationsCubit()); + sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), initializationCubit: sl())); - sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); 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 1d73d7cd..dd9b1c78 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -3,6 +3,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; @@ -19,7 +20,10 @@ import 'package:share_plus/share_plus.dart'; class BlogPostScreen extends StatefulWidget { final FeedBlogPostModel blogPost; - BlogPostScreen({super.key, required this.blogPost}); + BlogPostScreen({ + super.key, + required this.blogPost, + }); @override State createState() => _BlogPostScreenState(); @@ -185,6 +189,40 @@ class _BlogPostScreenState extends State { ), appBar: AppBar( title: Text(widget.blogPost.title), + actions: [ + PopupMenuButton( + onSelected: (value) { + if (value == 'delete') { + sl().deleteBlogPost(widget.blogPost.id); + context.read().loadFeedBlogPostList(); + Navigator.of(context).pop(); + } + if (value == 'edit') { + GoRouter.of(context).replaceNamed( + 'blogPostCreateScreen', + extra: widget.blogPost, + ); + } + }, + itemBuilder: (context) => [ + if (widget.blogPost.author.authorIsMe) + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + if (widget.blogPost.author.authorIsMe) + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + if (!widget.blogPost.author.authorIsMe) + PopupMenuItem( + value: 'report', + child: Text('Пожаловаться'), + ), + ], + ), + ], ), body: SingleChildScrollView( padding: EdgeInsets.only(bottom: 80), @@ -318,8 +356,7 @@ class _BlogPostScreenState extends State { ), ), SizedBox(height: 16), - if (state is CommentsStateLoading) - const Loader(), + if (state is CommentsStateLoading) const Loader(), if (state is CommentsStateLoaded) CommentsWidget( id: widget.blogPost.id.toString(), 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 81784e5f..90ee1e88 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 @@ -25,4 +25,28 @@ class BlogPostCreateCubit extends Cubit { ); emit(BlogPostCreateState.loaded()); } + + updateBlogPost({ + required int id, + String? title, + String? videoUrl, + String? description, + XFile? image, + String? oldImageUrl, + }) async { + emit(BlogPostCreateState.loading()); + await blogPostRepository.updateBlogPost( + id, + title, + videoUrl, + description, + image, + oldImageUrl, + ); + emit(BlogPostCreateState.loaded()); + } + + deleteBlogPost(int id) async { + await blogPostRepository.deleteBlogPost(id); + } } 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 ebe3593d..9e7a85d3 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,24 +1,25 @@ import 'dart:async'; import 'dart:io'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; class BlogPostCreateScreen extends StatefulWidget { - final FeedCubit? feedCubit; + final FeedBlogPostModel? feedBlogPostModel; const BlogPostCreateScreen({ - required this.feedCubit, + this.feedBlogPostModel, }) : super(); @override @@ -44,6 +45,11 @@ class _BlogPostCreateScreenState extends State { shrinkImage = visible; }); }); + if (widget.feedBlogPostModel != null) { + titleController.text = widget.feedBlogPostModel!.title; + descriptionController.text = widget.feedBlogPostModel!.description; + videoUrlController.text = widget.feedBlogPostModel!.videoUrl; + } } @override @@ -58,7 +64,8 @@ class _BlogPostCreateScreenState extends State { child: BlocListener( listener: (BuildContext context, BlogPostCreateState state) { if (state is BlogPostCreateStateLoaded) { - GoRouter.of(context).pop(); + context.read().loadFeedBlogPostList(); + Navigator.of(context).pop(); } }, child: _buildBlogPostCreate(), @@ -141,13 +148,23 @@ class _BlogPostCreateScreenState extends State { setState(() { isLoading = true; }); - await blogPostCubit.createBlogPost( - title: titleController.text, - description: descriptionController.text, - videoUrl: videoUrlController.text, - image: image, - ); - widget.feedCubit?.loadFeedBlogPostList(); + if (widget.feedBlogPostModel != null) { + await blogPostCubit.updateBlogPost( + id: widget.feedBlogPostModel!.id, + title: titleController.text, + description: descriptionController.text, + videoUrl: videoUrlController.text, + image: image, + oldImageUrl: widget.feedBlogPostModel!.image, + ); + } else { + await blogPostCubit.createBlogPost( + title: titleController.text, + description: descriptionController.text, + videoUrl: videoUrlController.text, + image: image, + ); + } }, ), ), @@ -175,9 +192,9 @@ class _BlogPostCreateScreenState extends State { if (videoUrlController.text.isEmpty) SizedBox(height: 8), if (videoUrlController.text.isEmpty) _buildAddPhoto(), SizedBox(height: 16), - if (image == null) Text('Ссылка на видео'), - if (image == null) SizedBox(height: 8), - if (image == null) + if (image == null && widget.feedBlogPostModel?.image == null) Text('Ссылка на видео'), + if (image == null && widget.feedBlogPostModel?.image == null) SizedBox(height: 8), + if (image == null && widget.feedBlogPostModel?.image == null) CustomTextFormField( controller: videoUrlController, ), @@ -195,7 +212,7 @@ class _BlogPostCreateScreenState extends State { }, child: Stack( children: [ - if (image == null) + if (image == null && widget.feedBlogPostModel?.image == null) AnimatedContainer( height: shrinkImage ? 52 : 128, curve: Curves.easeIn, @@ -233,6 +250,16 @@ class _BlogPostCreateScreenState extends State { ), ), ), + if (widget.feedBlogPostModel?.image != null && image == null) + Container( + width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image( + image: NetworkImage(baseUrl + '/storage/' + widget.feedBlogPostModel!.image), + ), + ), + ), ], ), ); diff --git a/lib/features/blog_post_create/repositories/blog_post_repository.dart b/lib/features/blog_post_create/repositories/blog_post_repository.dart index 1f6331a0..d7247be4 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -1,8 +1,11 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:http/http.dart' as http; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; class BlogPostRepository { final BlogPostService blogPostService; @@ -29,4 +32,58 @@ class BlogPostRepository { formData: formData, ); } + + Future downloadImage(String imageUrl) async { + try { + // Загрузка изображения + final response = await http.get(Uri.parse(baseUrl + '/storage/' + imageUrl)); + + if (response.statusCode == 200) { + // Получение временной директории + final directory = await getTemporaryDirectory(); + // Создание файла в временной директории + final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла + final file = File(filePath); + // Запись данных в файл + await file.writeAsBytes(response.bodyBytes); + + // Возвращаем объект XFile + return XFile(file.path); + } else { + print('Ошибка загрузки изображения: ${response.statusCode}'); + return null; + } + } catch (e) { + print('Произошла ошибка: $e'); + return null; + } + } + + Future updateBlogPost( + int id, + String? title, + String? videoUrl, + String? description, + XFile? image, + String? oldImageUrl, + ) async { + FormData formData = FormData.fromMap({ + 'title': title, + 'description': description, + 'state': 'active', + 'video_url': videoUrl, + 'image': image != null + ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name) + : oldImageUrl != null + ? await MultipartFile.fromFile((await downloadImage(oldImageUrl))?.path ?? '', filename: oldImageUrl.split('/').last) + : null, + }); + + return await blogPostService.updateBlogPost( + id: id, + formData: formData, + ); + } + + Future deleteBlogPost(int id) async => await blogPostService.deleteBlogPost(id: id); } diff --git a/lib/features/blog_post_create/services/blog_post_service.dart b/lib/features/blog_post_create/services/blog_post_service.dart index 8d09a84e..1d0934d5 100644 --- a/lib/features/blog_post_create/services/blog_post_service.dart +++ b/lib/features/blog_post_create/services/blog_post_service.dart @@ -12,4 +12,16 @@ abstract class BlogPostService { Future createBlogPost({ @Body() required FormData formData, }); + + @POST('/api/blogPosts/update/{id}') + @MultiPart() + Future updateBlogPost({ + @Path('id') required int id, + @Body() required FormData formData, + }); + + @POST('/api/blogPosts/delete/{id}') + Future deleteBlogPost({ + @Path('id') required int id, + }); } diff --git a/lib/features/blog_post_create/services/blog_post_service.g.dart b/lib/features/blog_post_create/services/blog_post_service.g.dart index de8f722f..33f4251a 100644 --- a/lib/features/blog_post_create/services/blog_post_service.g.dart +++ b/lib/features/blog_post_create/services/blog_post_service.g.dart @@ -47,6 +47,60 @@ class _BlogPostService implements BlogPostService { await _dio.fetch(_options); } + @override + Future updateBlogPost({ + required int id, + required FormData formData, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/blogPosts/update/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future deleteBlogPost({required int id}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/blogPosts/delete/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 949241af..1ccd21e6 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -14,6 +14,7 @@ class CreateDietCubit extends Cubit { final ProfileRepository profileRepository; final GlossaryRepository glossaryRepository; static const _dietStateValues = ['active', 'personal', 'draft']; + CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial()); void setDetails(String text) { @@ -236,7 +237,13 @@ class CreateDietCubit extends Cubit { ), details: state.dietData.details ?? '', dailySchedule: state.dietData.dailySchedule ?? [], - author: Author(authorSlug: authorProfile.slug, authorName: authorProfile.name, authorProfileIcon: authorProfile.profilePhotoUrl, authorIsMe: false), + author: Author( + authorSlug: authorProfile.slug, + authorName: authorProfile.name, + authorProfileIcon: authorProfile.profilePhotoUrl, + authorIsMe: false, + isEntitySubscribedByMe: false, + ), totalLikes: 0, totalViews: 0, totalComments: 0, diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 83df5eb0..e5839487 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -9,7 +9,7 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); + FeedCubit({required this.feedRepository,}) : super(const FeedState.loading()); loadFeedBlogPostList({ String? query, diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index f86430ef..cbaa0463 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -34,7 +34,7 @@ class FeedScreen extends StatefulWidget { } class _FeedScreenState extends State { - final FeedCubit feedCubit = sl()..loadFeedBlogPostList(); + late final FeedCubit feedCubit; final RecipesCubit recipesCubit = sl()..loadRecipesList(); final DietsCubit dietsCubit = sl()..loadDiets(); final PlansCubit plansCubit = sl()..loadPlans(); @@ -49,6 +49,8 @@ class _FeedScreenState extends State { @override void initState() { + feedCubit = context.read(); + feedCubit.loadFeedBlogPostList(); blogPostsScrollController.addListener(_onScrollBlogPosts); recipesScrollController.addListener(_onScrollRecipes); dietsScrollController.addListener(_onScrollDiets); @@ -444,7 +446,7 @@ class _FeedScreenState extends State { GoRouter.of(context).pushNamed(CreatePlanScreen.name); break; default: - GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedCubit); + GoRouter.of(context).pushNamed('blogPostCreateScreen'); } }, borderRadius: BorderRadius.circular(64), @@ -469,6 +471,7 @@ class _FeedScreenState extends State { width: MediaQuery.of(context).size.width - 84, child: TextFormField( controller: searchController, + autofocus: false, style: TextStyle( fontSize: 12, ), diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 8d547f95..ee2d66aa 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -18,7 +18,10 @@ import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { final FeedBlogPostModel feedBlogPostModel; - const BlogPostWidget({super.key, required this.feedBlogPostModel}); + const BlogPostWidget({ + super.key, + required this.feedBlogPostModel, + }); @override State createState() => _BlogPostWidgetState(); diff --git a/lib/main.dart b/lib/main.dart index d32df6a1..c166a0cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,6 +30,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), ], child: Portal( child: KeyboardDismisser( diff --git a/pubspec.yaml b/pubspec.yaml index 6a455e12..44dc695b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: file_picker: ^8.1.4 audioplayers: ^6.1.0 intl: ^0.19.0 - rive: ^0.13.18 + rive: ^0.13.19 mask_text_input_formatter: ^2.9.0 lottie: ^3.1.3 flutter_portal: ^1.1.4 @@ -58,6 +58,7 @@ dependencies: image: ^4.3.0 flutter_native_splash: ^2.4.3 dotted_border: ^2.1.0 + http: ^1.2.2 dev_dependencies: build_runner: ^2.4.13 From d370c9474af80bc40c8a4bebe50d6d5d5a51c17b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 15:17:22 +0300 Subject: [PATCH 04/12] feat(profile): fix subs --- lib/features/profile/models/sub_model.dart | 3 +- .../profile/models/sub_model.freezed.dart | 46 ++++++++++--------- lib/features/profile/models/sub_model.g.dart | 4 +- .../presentation/widgets/sub_widget.dart | 2 +- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/features/profile/models/sub_model.dart b/lib/features/profile/models/sub_model.dart index adce4bfb..4623e198 100644 --- a/lib/features/profile/models/sub_model.dart +++ b/lib/features/profile/models/sub_model.dart @@ -24,8 +24,7 @@ class SubModel with _$SubModel { required String name, required String city, required DateTime birthdate, - @JsonKey(name: 'is_subscribe') - required bool isSubscribe, + @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, required String email, @JsonKey(name: 'profile_photo_url') required String profilePhotoUrl, required String role, diff --git a/lib/features/profile/models/sub_model.freezed.dart b/lib/features/profile/models/sub_model.freezed.dart index b8938a71..5aeb0237 100644 --- a/lib/features/profile/models/sub_model.freezed.dart +++ b/lib/features/profile/models/sub_model.freezed.dart @@ -249,8 +249,8 @@ mixin _$SubModel { String get name => throw _privateConstructorUsedError; String get city => throw _privateConstructorUsedError; DateTime get birthdate => throw _privateConstructorUsedError; - @JsonKey(name: 'is_subscribe') - bool get isSubscribe => throw _privateConstructorUsedError; + @JsonKey(name: "is_entity_subscribed_by_me") + bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; String get email => throw _privateConstructorUsedError; @JsonKey(name: 'profile_photo_url') String get profilePhotoUrl => throw _privateConstructorUsedError; @@ -283,7 +283,7 @@ abstract class $SubModelCopyWith<$Res> { String name, String city, DateTime birthdate, - @JsonKey(name: 'is_subscribe') bool isSubscribe, + @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -312,7 +312,7 @@ class _$SubModelCopyWithImpl<$Res, $Val extends SubModel> Object? name = null, Object? city = null, Object? birthdate = null, - Object? isSubscribe = null, + Object? isEntitySubscribedByMe = null, Object? email = null, Object? profilePhotoUrl = null, Object? role = null, @@ -341,9 +341,9 @@ class _$SubModelCopyWithImpl<$Res, $Val extends SubModel> ? _value.birthdate : birthdate // ignore: cast_nullable_to_non_nullable as DateTime, - isSubscribe: null == isSubscribe - ? _value.isSubscribe - : isSubscribe // ignore: cast_nullable_to_non_nullable + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable as bool, email: null == email ? _value.email @@ -387,7 +387,7 @@ abstract class _$$SubModelImplCopyWith<$Res> String name, String city, DateTime birthdate, - @JsonKey(name: 'is_subscribe') bool isSubscribe, + @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -414,7 +414,7 @@ class __$$SubModelImplCopyWithImpl<$Res> Object? name = null, Object? city = null, Object? birthdate = null, - Object? isSubscribe = null, + Object? isEntitySubscribedByMe = null, Object? email = null, Object? profilePhotoUrl = null, Object? role = null, @@ -443,9 +443,9 @@ class __$$SubModelImplCopyWithImpl<$Res> ? _value.birthdate : birthdate // ignore: cast_nullable_to_non_nullable as DateTime, - isSubscribe: null == isSubscribe - ? _value.isSubscribe - : isSubscribe // ignore: cast_nullable_to_non_nullable + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable as bool, email: null == email ? _value.email @@ -484,7 +484,8 @@ class _$SubModelImpl implements _SubModel { required this.name, required this.city, required this.birthdate, - @JsonKey(name: 'is_subscribe') required this.isSubscribe, + @JsonKey(name: "is_entity_subscribed_by_me") + required this.isEntitySubscribedByMe, required this.email, @JsonKey(name: 'profile_photo_url') required this.profilePhotoUrl, required this.role, @@ -506,8 +507,8 @@ class _$SubModelImpl implements _SubModel { @override final DateTime birthdate; @override - @JsonKey(name: 'is_subscribe') - final bool isSubscribe; + @JsonKey(name: "is_entity_subscribed_by_me") + final bool isEntitySubscribedByMe; @override final String email; @override @@ -527,7 +528,7 @@ class _$SubModelImpl implements _SubModel { @override String toString() { - return 'SubModel(id: $id, slug: $slug, name: $name, city: $city, birthdate: $birthdate, isSubscribe: $isSubscribe, email: $email, profilePhotoUrl: $profilePhotoUrl, role: $role, isPublicProfile: $isPublicProfile, createdAt: $createdAt, updatedAt: $updatedAt)'; + return 'SubModel(id: $id, slug: $slug, name: $name, city: $city, birthdate: $birthdate, isEntitySubscribedByMe: $isEntitySubscribedByMe, email: $email, profilePhotoUrl: $profilePhotoUrl, role: $role, isPublicProfile: $isPublicProfile, createdAt: $createdAt, updatedAt: $updatedAt)'; } @override @@ -541,8 +542,8 @@ class _$SubModelImpl implements _SubModel { (identical(other.city, city) || other.city == city) && (identical(other.birthdate, birthdate) || other.birthdate == birthdate) && - (identical(other.isSubscribe, isSubscribe) || - other.isSubscribe == isSubscribe) && + (identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) || + other.isEntitySubscribedByMe == isEntitySubscribedByMe) && (identical(other.email, email) || other.email == email) && (identical(other.profilePhotoUrl, profilePhotoUrl) || other.profilePhotoUrl == profilePhotoUrl) && @@ -564,7 +565,7 @@ class _$SubModelImpl implements _SubModel { name, city, birthdate, - isSubscribe, + isEntitySubscribedByMe, email, profilePhotoUrl, role, @@ -595,7 +596,8 @@ abstract class _SubModel implements SubModel { required final String name, required final String city, required final DateTime birthdate, - @JsonKey(name: 'is_subscribe') required final bool isSubscribe, + @JsonKey(name: "is_entity_subscribed_by_me") + required final bool isEntitySubscribedByMe, required final String email, @JsonKey(name: 'profile_photo_url') required final String profilePhotoUrl, required final String role, @@ -618,8 +620,8 @@ abstract class _SubModel implements SubModel { @override DateTime get birthdate; @override - @JsonKey(name: 'is_subscribe') - bool get isSubscribe; + @JsonKey(name: "is_entity_subscribed_by_me") + bool get isEntitySubscribedByMe; @override String get email; @override diff --git a/lib/features/profile/models/sub_model.g.dart b/lib/features/profile/models/sub_model.g.dart index a6536132..e3c9397e 100644 --- a/lib/features/profile/models/sub_model.g.dart +++ b/lib/features/profile/models/sub_model.g.dart @@ -31,7 +31,7 @@ _$SubModelImpl _$$SubModelImplFromJson(Map json) => name: json['name'] as String, city: json['city'] as String, birthdate: DateTime.parse(json['birthdate'] as String), - isSubscribe: json['is_subscribe'] as bool, + isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool, email: json['email'] as String, profilePhotoUrl: json['profile_photo_url'] as String, role: json['role'] as String, @@ -47,7 +47,7 @@ Map _$$SubModelImplToJson(_$SubModelImpl instance) => 'name': instance.name, 'city': instance.city, 'birthdate': instance.birthdate.toIso8601String(), - 'is_subscribe': instance.isSubscribe, + 'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe, 'email': instance.email, 'profile_photo_url': instance.profilePhotoUrl, 'role': instance.role, diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index 0a4e2d40..ad42573f 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -66,7 +66,7 @@ class _SubWidgetState extends State { ), const Spacer(), SubActionButtonWidget( - subActionType: widget.subModel.isSubscribe ? SubActionType.SendMessage : SubActionType.Subscribe, + subActionType: widget.subModel.isEntitySubscribedByMe ? SubActionType.SendMessage : SubActionType.Subscribe, slug: widget.subModel.slug, ), ], From 6a98bcb49262ea68ac5fb973e1e36fbfb6777d89 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 15:36:43 +0300 Subject: [PATCH 05/12] =?UTF-8?q?feat(diary):=20=D0=9F=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=20=D1=84=D0=BE=D0=BA=D1=83=D1=81=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=B8=D1=87=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=BE=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=8B=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=B5=20=D0=B8=D0=B7=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B3=D0=BB=D0=BE=D1=81=D1=81=D0=B0=D1=80=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/diary/presentation/widgets/add_product_modal.dart | 3 ++- lib/features/profile/services/profile_service.dart | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index 6dfbc53c..65852357 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -86,7 +86,7 @@ class _AddProductModalState extends State { hintText: 'Введите название продукта/блюда', onChanged: (query) { EasyDebounce.debounce( - 'client-typing', + 'glossary-search', Duration(milliseconds: 250), () async { glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); @@ -133,6 +133,7 @@ class _AddProductModalState extends State { }); glossariesList = await widget.diaryCubit.getGlossaryListByQuery(glossariesList[i].name); setState(() {}); + FocusManager.instance.primaryFocus?.nextFocus(); }, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 1665d9e0..7d9f6480 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; import 'package:retrofit/retrofit.dart'; + part 'profile_service.g.dart'; @RestApi() From 92a3a60636bd493dc76bbfd51490ab9491f0d501 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 17:12:13 +0300 Subject: [PATCH 06/12] =?UTF-8?q?feat(profile):=20=D0=9F=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B8=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=87=D0=B8=D0=BA=D0=B8,=20=D0=90=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=BA=D0=B0=20(=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=B8=D1=82=D1=8C/=D1=83=D0=B4=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C/=D0=BF=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D1=8C),=20=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8F=20(=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=BA=D0=B0),=20=D0=9F=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D1=8C=D1=81=D1=8F=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/locator_service.dart | 2 +- .../profile_avatar_cubit.dart | 5 +- .../widgets/bottom_navigation_bar_widget.dart | 24 +- .../profile/models/profile_model.dart | 1 + .../profile/models/profile_model.freezed.dart | 29 +- .../profile/models/profile_model.g.dart | 2 + .../profile/models/sub_model.freezed.dart | 14 +- .../bloc/profile_cubit/profile_cubit.dart | 8 + .../presentation/pages/profile_screen.dart | 254 +++++++++++++++--- .../repositories/profile_repository.dart | 10 + .../profile/services/profile_service.dart | 8 + .../profile/services/profile_service.g.dart | 33 +++ lib/main.dart | 2 + 13 files changed, 328 insertions(+), 64 deletions(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9f456ad2..2631ff92 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -61,6 +61,7 @@ Future init() async { sl.registerLazySingleton(() => NewMessagesCubit()); sl.registerLazySingleton(() => NotificationsCubit()); sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); + sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); @@ -68,7 +69,6 @@ Future init() async { sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); - sl.registerFactory(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerFactory(() => RecipeCreateCubit(recipeRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl())); diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index 2990a507..4655d87d 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -10,10 +10,9 @@ class ProfileAvatarCubit extends Cubit { }) : super(const ProfileAvatarState.loading()); getMyProfile() async { + emit(const ProfileAvatarState.loading()); try { - emit(ProfileAvatarState.loaded( - profile: await profileRepository.getMyProfile(), - ),); + emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile())); } catch (e) { emit(ProfileAvatarState.error(errorMessage: e.toString())); } diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index fc7c278e..48b7474d 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -7,7 +7,6 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/noti import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; -import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -72,22 +71,19 @@ class _BottomNavigationBarWidgetState extends State { items: [ BottomNavigationBarItem( label: '', - icon: BlocProvider( - create: (context) => sl()..getMyProfile(), - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return CircleAvatar( - backgroundImage: NetworkImage(state.profile.profilePhotoUrl), - radius: 16, - ); - } + icon: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { return CircleAvatar( + backgroundImage: NetworkImage(state.profile.profilePhotoUrl), radius: 16, - backgroundColor: Colors.black12, ); - }, - ), + } + return CircleAvatar( + radius: 16, + backgroundColor: Colors.black12, + ); + }, ), ), BottomNavigationBarItem( diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 075604fc..df06dee5 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -15,6 +15,7 @@ class ProfileModel with _$ProfileModel { @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") required bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, @JsonKey(name: "profile_photo_url") required String profilePhotoUrl, @JsonKey(name: "role") required String role, diff --git a/lib/features/profile/models/profile_model.freezed.dart b/lib/features/profile/models/profile_model.freezed.dart index 862e19e6..d5ec2486 100644 --- a/lib/features/profile/models/profile_model.freezed.dart +++ b/lib/features/profile/models/profile_model.freezed.dart @@ -34,6 +34,8 @@ mixin _$ProfileModel { DateTime? get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: "is_public_profile") bool get isPublicProfile => throw _privateConstructorUsedError; + @JsonKey(name: 'is_entity_subscribed_by_me') + bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; @JsonKey(name: "slug") String get slug => throw _privateConstructorUsedError; @JsonKey(name: "profile_photo_url") @@ -73,6 +75,7 @@ abstract class $ProfileModelCopyWith<$Res> { @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, @JsonKey(name: "slug") String slug, @JsonKey(name: "profile_photo_url") String profilePhotoUrl, @JsonKey(name: "role") String role, @@ -103,6 +106,7 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> Object? createdAt = freezed, Object? updatedAt = freezed, Object? isPublicProfile = null, + Object? isEntitySubscribedByMe = null, Object? slug = null, Object? profilePhotoUrl = null, Object? role = null, @@ -139,6 +143,10 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> ? _value.isPublicProfile : isPublicProfile // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -183,6 +191,7 @@ abstract class _$$ProfileModelImplCopyWith<$Res> @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, @JsonKey(name: "slug") String slug, @JsonKey(name: "profile_photo_url") String profilePhotoUrl, @JsonKey(name: "role") String role, @@ -211,6 +220,7 @@ class __$$ProfileModelImplCopyWithImpl<$Res> Object? createdAt = freezed, Object? updatedAt = freezed, Object? isPublicProfile = null, + Object? isEntitySubscribedByMe = null, Object? slug = null, Object? profilePhotoUrl = null, Object? role = null, @@ -247,6 +257,10 @@ class __$$ProfileModelImplCopyWithImpl<$Res> ? _value.isPublicProfile : isPublicProfile // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -286,6 +300,8 @@ class _$ProfileModelImpl implements _ProfileModel { @JsonKey(name: "created_at") this.createdAt, @JsonKey(name: "updated_at") this.updatedAt, @JsonKey(name: "is_public_profile") required this.isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') + required this.isEntitySubscribedByMe, @JsonKey(name: "slug") required this.slug, @JsonKey(name: "profile_photo_url") required this.profilePhotoUrl, @JsonKey(name: "role") required this.role, @@ -323,6 +339,9 @@ class _$ProfileModelImpl implements _ProfileModel { @JsonKey(name: "is_public_profile") final bool isPublicProfile; @override + @JsonKey(name: 'is_entity_subscribed_by_me') + final bool isEntitySubscribedByMe; + @override @JsonKey(name: "slug") final String slug; @override @@ -360,7 +379,7 @@ class _$ProfileModelImpl implements _ProfileModel { @override String toString() { - return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers)'; + return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, isEntitySubscribedByMe: $isEntitySubscribedByMe, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers)'; } @override @@ -379,6 +398,8 @@ class _$ProfileModelImpl implements _ProfileModel { other.updatedAt == updatedAt) && (identical(other.isPublicProfile, isPublicProfile) || other.isPublicProfile == isPublicProfile) && + (identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) || + other.isEntitySubscribedByMe == isEntitySubscribedByMe) && (identical(other.slug, slug) || other.slug == slug) && (identical(other.profilePhotoUrl, profilePhotoUrl) || other.profilePhotoUrl == profilePhotoUrl) && @@ -401,6 +422,7 @@ class _$ProfileModelImpl implements _ProfileModel { createdAt, updatedAt, isPublicProfile, + isEntitySubscribedByMe, slug, profilePhotoUrl, role, @@ -433,6 +455,8 @@ abstract class _ProfileModel implements ProfileModel { @JsonKey(name: "created_at") final DateTime? createdAt, @JsonKey(name: "updated_at") final DateTime? updatedAt, @JsonKey(name: "is_public_profile") required final bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') + required final bool isEntitySubscribedByMe, @JsonKey(name: "slug") required final String slug, @JsonKey(name: "profile_photo_url") required final String profilePhotoUrl, @JsonKey(name: "role") required final String role, @@ -468,6 +492,9 @@ abstract class _ProfileModel implements ProfileModel { @JsonKey(name: "is_public_profile") bool get isPublicProfile; @override + @JsonKey(name: 'is_entity_subscribed_by_me') + bool get isEntitySubscribedByMe; + @override @JsonKey(name: "slug") String get slug; @override diff --git a/lib/features/profile/models/profile_model.g.dart b/lib/features/profile/models/profile_model.g.dart index aa2431ca..47755454 100644 --- a/lib/features/profile/models/profile_model.g.dart +++ b/lib/features/profile/models/profile_model.g.dart @@ -21,6 +21,7 @@ _$ProfileModelImpl _$$ProfileModelImplFromJson(Map json) => ? null : DateTime.parse(json['updated_at'] as String), isPublicProfile: json['is_public_profile'] as bool, + isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool, slug: json['slug'] as String, profilePhotoUrl: json['profile_photo_url'] as String, role: json['role'] as String, @@ -44,6 +45,7 @@ Map _$$ProfileModelImplToJson(_$ProfileModelImpl instance) => 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'is_public_profile': instance.isPublicProfile, + 'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe, 'slug': instance.slug, 'profile_photo_url': instance.profilePhotoUrl, 'role': instance.role, diff --git a/lib/features/profile/models/sub_model.freezed.dart b/lib/features/profile/models/sub_model.freezed.dart index 5aeb0237..34d6ae49 100644 --- a/lib/features/profile/models/sub_model.freezed.dart +++ b/lib/features/profile/models/sub_model.freezed.dart @@ -249,7 +249,7 @@ mixin _$SubModel { String get name => throw _privateConstructorUsedError; String get city => throw _privateConstructorUsedError; DateTime get birthdate => throw _privateConstructorUsedError; - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; String get email => throw _privateConstructorUsedError; @JsonKey(name: 'profile_photo_url') @@ -283,7 +283,7 @@ abstract class $SubModelCopyWith<$Res> { String name, String city, DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -387,7 +387,7 @@ abstract class _$$SubModelImplCopyWith<$Res> String name, String city, DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -484,7 +484,7 @@ class _$SubModelImpl implements _SubModel { required this.name, required this.city, required this.birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') required this.isEntitySubscribedByMe, required this.email, @JsonKey(name: 'profile_photo_url') required this.profilePhotoUrl, @@ -507,7 +507,7 @@ class _$SubModelImpl implements _SubModel { @override final DateTime birthdate; @override - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') final bool isEntitySubscribedByMe; @override final String email; @@ -596,7 +596,7 @@ abstract class _SubModel implements SubModel { required final String name, required final String city, required final DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') required final bool isEntitySubscribedByMe, required final String email, @JsonKey(name: 'profile_photo_url') required final String profilePhotoUrl, @@ -620,7 +620,7 @@ abstract class _SubModel implements SubModel { @override DateTime get birthdate; @override - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') bool get isEntitySubscribedByMe; @override String get email; diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index bdd4e0f4..235a16bf 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -80,4 +83,9 @@ class ProfileCubit extends Cubit { rethrow; } } + + uploadPhoto(File file) async { + await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file)); + return await getMyProfile(); + } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 51825cfd..2abd3feb 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,22 +1,32 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.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/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:share_plus/share_plus.dart'; class ProfileScreen extends StatefulWidget { final Author? author; @@ -40,6 +50,8 @@ class _ProfileScreenState extends State { List circumferencePoints = []; int _selectedChipIndex = 0; int _selectedIntervalChipIndex = 0; + late bool canSub; + bool isFirstFetch = true; @override void initState() { @@ -57,6 +69,10 @@ class _ProfileScreenState extends State { circumferencePoints = state.circumferencePoints!; weightPoints = state.weightPoints!; } + if (isFirstFetch) { + canSub = !state.profile.isEntitySubscribedByMe; + isFirstFetch = false; + } return widget.author == null ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -99,7 +115,7 @@ class _ProfileScreenState extends State { _header(state), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), const SizedBox(height: 8), - _messageAndSubscribe(), + _messageAndSubscribe(state), const SizedBox(height: 16), ], ), @@ -692,16 +708,96 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.all(16), child: Row( children: [ - CircleAvatar( - radius: 32, - backgroundImage: NetworkImage(state.profile.profilePhotoUrl), + PopupMenuButton( + onSelected: (value) { + if (value == 'view') { + context.pushTransparentRoute( + rootNavigator: true, + PhotoViewScreen( + imageUrls: [state.profile.profilePhotoUrl], + index: 0, + ), + ); + } + if (value == 'replace') { + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.camera); + if (image != null) { + await profileCubit.uploadPhoto(File(image.path)); + await sl().getMyProfile(); + } + }, + child: const Row( + children: const [ + Icon(Icons.photo_camera), + SizedBox(width: 8), + Text('Фото'), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + await profileCubit.uploadPhoto(File(image.path)); + await sl().getMyProfile(); + } + }, + child: const Row( + children: const [ + Icon(Icons.photo), + SizedBox(width: 8), + Text('Галерея'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'view', + child: Text('Посмотреть'), + ), + PopupMenuItem( + value: 'replace', + child: Text('Заменить'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + child: Hero( + tag: state.profile.profilePhotoUrl, + child: CircleAvatar( + radius: 32, + backgroundImage: NetworkImage(state.profile.profilePhotoUrl), + ), + ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - state.profile.name, + state.profileDetails.firstName + ' ' + state.profileDetails.lastName, style: TextStyle( color: Colors.black, fontWeight: FontWeight.w600, @@ -710,37 +806,110 @@ class _ProfileScreenState extends State { ), ), const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.info_outline, - size: 20, - color: Color(0xFF6D6F71), - ), - const SizedBox(width: 4), - Text( - 'Дополнительно', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w500, + GestureDetector( + onTap: () { + showModalBottomSheet( + useRootNavigator: true, + showDragHandle: true, + useSafeArea: true, + context: context, + builder: (context) => Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Подробнее', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFF323F4B), + ), + ), + const SizedBox(height: 16), + _additional('Дата рожденя: ', '${state.profileDetails.birthdate}'), + const SizedBox(height: 12), + _additional('Страна: ', '${state.profileDetails.country}'), + const SizedBox(height: 12), + _additional('Город: ', '${state.profileDetails.city}'), + const SizedBox(height: 12), + _additional('Язык общения: ', '${state.profileDetails.lang}'), + const SizedBox(height: 12), + _additional('В Eatvio с: ', '${state.profile.createdAt?.format('d.MM.yyyy', 'ru')}'), + const SizedBox(height: 12), + ], + ), ), - ), - ], + ); + }, + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 20, + color: Color(0xFF6D6F71), + ), + const SizedBox(width: 4), + Text( + 'Дополнительно', + style: TextStyle( + color: Color(0xFF6D6F71), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ], ), const Spacer(), - IconButton( - onPressed: () {}, - icon: Icon(Icons.more_horiz_rounded), + PopupMenuButton( + onSelected: (value) { + Share.shareUri( + Uri.parse( + baseUrl + '/ru/profile/' + state.profile.slug, + ), + ); + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'share', + child: Text('Поделиться'), + ), + ], + child: Icon(Icons.more_horiz_rounded), ), ], ), ); } - _messageAndSubscribe() { + _additional(String title, String value) { + return Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF6D6F71), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF323F4B), + ), + ), + ], + ); + } + + _messageAndSubscribe(ProfileStateLoaded state) { return Row( children: [ SizedBox(width: 16), @@ -768,18 +937,26 @@ class _ProfileScreenState extends State { ), ), SizedBox(width: 8), - Container( - alignment: Alignment.center, - padding: EdgeInsets.all(8), - width: (MediaQuery.of(context).size.width - 40) / 2, - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(16), - ), - child: Text( - 'Подписаться', - style: TextStyle( - color: Colors.white, + GestureDetector( + onTap: () { + setState(() { + canSub = !canSub; + }); + canSub ? sl().unsubscribe(state.profile.slug) : sl().subscribe(state.profile.slug); + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.all(8), + width: (MediaQuery.of(context).size.width - 40) / 2, + decoration: BoxDecoration( + color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + !canSub ? 'Отписаться' : 'Подписаться', + style: TextStyle( + color: !canSub ? Colors.black : Colors.white, + ), ), ), ), @@ -826,7 +1003,8 @@ class _ProfileScreenState extends State { return ImtChartWidget( weightPoints: weightPoints, ); - default: return SizedBox.shrink(); + default: + return SizedBox.shrink(); } }, ), diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 022a768e..5bb2e00f 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -145,4 +147,12 @@ class ProfileRepository { rethrow; } } + + Future uploadPhoto(File file) async { + try { + return await profileService.uploadPhoto(photo: file); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 7d9f6480..4517fee2 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; @@ -83,4 +85,10 @@ abstract class ProfileService { Future unsubscribe({ @Path() required String slug, }); + + @POST('/api/profile/photo/upload') + @MultiPart() + Future uploadPhoto({ + @Part(name: 'photo') required File photo, + }); } diff --git a/lib/features/profile/services/profile_service.g.dart b/lib/features/profile/services/profile_service.g.dart index 366ab7de..44a66b83 100644 --- a/lib/features/profile/services/profile_service.g.dart +++ b/lib/features/profile/services/profile_service.g.dart @@ -504,6 +504,39 @@ class _ProfileService implements ProfileService { await _dio.fetch(_options); } + @override + Future uploadPhoto({required File photo}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = FormData(); + _data.files.add(MapEntry( + 'photo', + MultipartFile.fromFileSync( + photo.path, + filename: photo.path.split(Platform.pathSeparator).last, + ), + )); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/profile/photo/upload', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/main.dart b/lib/main.dart index c166a0cf..195c45de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/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/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..getMyProfile()), ], child: Portal( child: KeyboardDismisser( From 66f6f0ed91db9241090573c092770b817d14168d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 17:24:09 +0300 Subject: [PATCH 07/12] =?UTF-8?q?feat(profile):=20=D0=9E=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F=20(=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bloc/profile_cubit/profile_cubit.dart | 28 ++++++++------- .../repositories/profile_repository.dart | 8 +++++ .../profile/services/profile_service.dart | 5 +++ .../profile/services/profile_service.g.dart | 34 +++++++++++++++++++ 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 235a16bf..5a322425 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -18,11 +18,13 @@ class ProfileCubit extends Cubit { getProfileBySlug(String slug) async { try { - emit(ProfileState.loaded( - profile: await profileRepository.getProfileBySlug(slug), - profileDetails: await profileRepository.getMyProfileDetails(), - blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug), - ),); + emit( + ProfileState.loaded( + profile: await profileRepository.getProfileBySlug(slug), + profileDetails: await profileRepository.getProfileDetailsBySlug(slug), + blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug), + ), + ); } catch (e) { emit(ProfileState.error(errorMessage: e.toString())); } @@ -34,13 +36,15 @@ class ProfileCubit extends Cubit { _currentPeriod = period; try { final profile = await profileRepository.getMyProfile(); - emit(ProfileState.loaded( - profile: profile, - profileDetails: await profileRepository.getMyProfileDetails(), - blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug), - weightPoints: await profileRepository.getWeight(period: period), - circumferencePoints: await profileRepository.getCircumferences(period: period), - ),); + emit( + ProfileState.loaded( + profile: profile, + profileDetails: await profileRepository.getMyProfileDetails(), + blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug), + weightPoints: await profileRepository.getWeight(period: period), + circumferencePoints: await profileRepository.getCircumferences(period: period), + ), + ); } catch (e) { emit(ProfileState.error(errorMessage: e.toString())); } diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 5bb2e00f..96825109 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -22,6 +22,14 @@ class ProfileRepository { } } + Future getProfileDetailsBySlug(String slug) async { + try { + return await profileService.getProfileDetailsBySlug(slug: slug); + } catch (e) { + rethrow; + } + } + Future getMyProfile() async { try { return await profileService.getMyProfile(); diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 4517fee2..8cab7056 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -25,6 +25,11 @@ abstract class ProfileService { @GET('/api/profile/detailz') Future getMyProfileDetails(); + @GET('/api/profile/detailz/{slug}') + Future getProfileDetailsBySlug({ + @Path() required String slug, + }); + @POST('/api/profile/addCircumference') Future addCircumference({ @Part(name: 'chest_circumference') required int chestCircumference, diff --git a/lib/features/profile/services/profile_service.g.dart b/lib/features/profile/services/profile_service.g.dart index 44a66b83..a0e72b67 100644 --- a/lib/features/profile/services/profile_service.g.dart +++ b/lib/features/profile/services/profile_service.g.dart @@ -120,6 +120,40 @@ class _ProfileService implements ProfileService { return _value; } + @override + Future getProfileDetailsBySlug( + {required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/profile/detailz/${slug}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late ProfileDetailsModel _value; + try { + _value = ProfileDetailsModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + @override Future addCircumference({ required int chestCircumference, From a4b271191423fdd482e76248d69e044a8f2f71d8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 17:35:32 +0300 Subject: [PATCH 08/12] fix: comments and feed --- .../bloc/feed_cubit/feed_cubit.dart | 4 +++- .../feed/presentation/pages/feed_screen.dart | 3 +-- .../presentation/widgets/comments_widget.dart | 17 ++++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index e5839487..802f9a18 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -9,7 +9,9 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({required this.feedRepository,}) : super(const FeedState.loading()); + FeedCubit({ + required this.feedRepository, + }) : super(const FeedState.loading()); loadFeedBlogPostList({ String? query, diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index cbaa0463..611a9015 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -34,7 +34,7 @@ class FeedScreen extends StatefulWidget { } class _FeedScreenState extends State { - late final FeedCubit feedCubit; + final FeedCubit feedCubit = sl(); final RecipesCubit recipesCubit = sl()..loadRecipesList(); final DietsCubit dietsCubit = sl()..loadDiets(); final PlansCubit plansCubit = sl()..loadPlans(); @@ -49,7 +49,6 @@ class _FeedScreenState extends State { @override void initState() { - feedCubit = context.read(); feedCubit.loadFeedBlogPostList(); blogPostsScrollController.addListener(_onScrollBlogPosts); recipesScrollController.addListener(_onScrollRecipes); diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 96391ca8..fd931541 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -77,13 +77,16 @@ class _CommentsWidgetState extends State { ), ), SizedBox(height: 4), - Text( - comment.body, - style: TextStyle( - height: 1, - color: appTheme.black, - fontSize: 12, - fontWeight: FontWeight.w500, + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: Text( + comment.body, + style: TextStyle( + height: 1, + color: appTheme.black, + fontSize: 12, + fontWeight: FontWeight.w500, + ), ), ), SizedBox(height: 4), From 6cf6e32fe690332857b7507e330e258b65cceb9d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 17:52:21 +0300 Subject: [PATCH 09/12] feat(profile): social links --- .../profile/models/profile_model.dart | 16 + .../profile/models/profile_model.freezed.dart | 365 +++++++++++++++++- .../profile/models/profile_model.g.dart | 29 ++ .../presentation/pages/profile_screen.dart | 34 +- 4 files changed, 433 insertions(+), 11 deletions(-) diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index df06dee5..374111cc 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -22,11 +22,27 @@ class ProfileModel with _$ProfileModel { @JsonKey(name: "badges") required List badges, @JsonKey(name: "subscriptions") required List subscriptions, @JsonKey(name: "subscribers") required List subscribers, + @JsonKey(name: "education_resource") EducationResource? educationResource, }) = _ProfileModel; factory ProfileModel.fromJson(Map json) => _$ProfileModelFromJson(json); } +@freezed +class EducationResource with _$EducationResource { + const factory EducationResource({ + @JsonKey(name: "has_medical_education") int? hasMedical_Education, + @JsonKey(name: "vk_link") String? vkLink, + @JsonKey(name: "insta_link") String? instaLink, + @JsonKey(name: "fb_link") String? fbLink, + @JsonKey(name: "tg_link") String? tgLink, + @JsonKey(name: "personal_site_link") String? personalSiteLink, + @JsonKey(name: "request_state") String? requestState, + }) = _EducationResource; + + factory EducationResource.fromJson(Map json) => _$EducationResourceFromJson(json); +} + @freezed class ProfileDetailsModel with _$ProfileDetailsModel { const factory ProfileDetailsModel({ diff --git a/lib/features/profile/models/profile_model.freezed.dart b/lib/features/profile/models/profile_model.freezed.dart index d5ec2486..95be8ea8 100644 --- a/lib/features/profile/models/profile_model.freezed.dart +++ b/lib/features/profile/models/profile_model.freezed.dart @@ -50,6 +50,9 @@ mixin _$ProfileModel { @JsonKey(name: "subscribers") List get subscribers => throw _privateConstructorUsedError; + @JsonKey(name: "education_resource") + EducationResource? get educationResource => + throw _privateConstructorUsedError; /// Serializes this ProfileModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -81,7 +84,11 @@ abstract class $ProfileModelCopyWith<$Res> { @JsonKey(name: "role") String role, @JsonKey(name: "badges") List badges, @JsonKey(name: "subscriptions") List subscriptions, - @JsonKey(name: "subscribers") List subscribers}); + @JsonKey(name: "subscribers") List subscribers, + @JsonKey(name: "education_resource") + EducationResource? educationResource}); + + $EducationResourceCopyWith<$Res>? get educationResource; } /// @nodoc @@ -113,6 +120,7 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> Object? badges = null, Object? subscriptions = null, Object? subscribers = null, + Object? educationResource = freezed, }) { return _then(_value.copyWith( id: null == id @@ -171,8 +179,26 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> ? _value.subscribers : subscribers // ignore: cast_nullable_to_non_nullable as List, + educationResource: freezed == educationResource + ? _value.educationResource + : educationResource // ignore: cast_nullable_to_non_nullable + as EducationResource?, ) as $Val); } + + /// Create a copy of ProfileModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $EducationResourceCopyWith<$Res>? get educationResource { + if (_value.educationResource == null) { + return null; + } + + return $EducationResourceCopyWith<$Res>(_value.educationResource!, (value) { + return _then(_value.copyWith(educationResource: value) as $Val); + }); + } } /// @nodoc @@ -197,7 +223,12 @@ abstract class _$$ProfileModelImplCopyWith<$Res> @JsonKey(name: "role") String role, @JsonKey(name: "badges") List badges, @JsonKey(name: "subscriptions") List subscriptions, - @JsonKey(name: "subscribers") List subscribers}); + @JsonKey(name: "subscribers") List subscribers, + @JsonKey(name: "education_resource") + EducationResource? educationResource}); + + @override + $EducationResourceCopyWith<$Res>? get educationResource; } /// @nodoc @@ -227,6 +258,7 @@ class __$$ProfileModelImplCopyWithImpl<$Res> Object? badges = null, Object? subscriptions = null, Object? subscribers = null, + Object? educationResource = freezed, }) { return _then(_$ProfileModelImpl( id: null == id @@ -285,6 +317,10 @@ class __$$ProfileModelImplCopyWithImpl<$Res> ? _value._subscribers : subscribers // ignore: cast_nullable_to_non_nullable as List, + educationResource: freezed == educationResource + ? _value.educationResource + : educationResource // ignore: cast_nullable_to_non_nullable + as EducationResource?, )); } } @@ -309,7 +345,8 @@ class _$ProfileModelImpl implements _ProfileModel { @JsonKey(name: "subscriptions") required final List subscriptions, @JsonKey(name: "subscribers") - required final List subscribers}) + required final List subscribers, + @JsonKey(name: "education_resource") this.educationResource}) : _badges = badges, _subscriptions = subscriptions, _subscribers = subscribers; @@ -377,9 +414,13 @@ class _$ProfileModelImpl implements _ProfileModel { return EqualUnmodifiableListView(_subscribers); } + @override + @JsonKey(name: "education_resource") + final EducationResource? educationResource; + @override String toString() { - return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, isEntitySubscribedByMe: $isEntitySubscribedByMe, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers)'; + return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, isEntitySubscribedByMe: $isEntitySubscribedByMe, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers, educationResource: $educationResource)'; } @override @@ -408,7 +449,9 @@ class _$ProfileModelImpl implements _ProfileModel { const DeepCollectionEquality() .equals(other._subscriptions, _subscriptions) && const DeepCollectionEquality() - .equals(other._subscribers, _subscribers)); + .equals(other._subscribers, _subscribers) && + (identical(other.educationResource, educationResource) || + other.educationResource == educationResource)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -428,7 +471,8 @@ class _$ProfileModelImpl implements _ProfileModel { role, const DeepCollectionEquality().hash(_badges), const DeepCollectionEquality().hash(_subscriptions), - const DeepCollectionEquality().hash(_subscribers)); + const DeepCollectionEquality().hash(_subscribers), + educationResource); /// Create a copy of ProfileModel /// with the given fields replaced by the non-null parameter values. @@ -464,8 +508,9 @@ abstract class _ProfileModel implements ProfileModel { @JsonKey(name: "subscriptions") required final List subscriptions, @JsonKey(name: "subscribers") - required final List - subscribers}) = _$ProfileModelImpl; + required final List subscribers, + @JsonKey(name: "education_resource") + final EducationResource? educationResource}) = _$ProfileModelImpl; factory _ProfileModel.fromJson(Map json) = _$ProfileModelImpl.fromJson; @@ -512,6 +557,9 @@ abstract class _ProfileModel implements ProfileModel { @override @JsonKey(name: "subscribers") List get subscribers; + @override + @JsonKey(name: "education_resource") + EducationResource? get educationResource; /// Create a copy of ProfileModel /// with the given fields replaced by the non-null parameter values. @@ -521,6 +569,307 @@ abstract class _ProfileModel implements ProfileModel { throw _privateConstructorUsedError; } +EducationResource _$EducationResourceFromJson(Map json) { + return _EducationResource.fromJson(json); +} + +/// @nodoc +mixin _$EducationResource { + @JsonKey(name: "has_medical_education") + int? get hasMedical_Education => throw _privateConstructorUsedError; + @JsonKey(name: "vk_link") + String? get vkLink => throw _privateConstructorUsedError; + @JsonKey(name: "insta_link") + String? get instaLink => throw _privateConstructorUsedError; + @JsonKey(name: "fb_link") + String? get fbLink => throw _privateConstructorUsedError; + @JsonKey(name: "tg_link") + String? get tgLink => throw _privateConstructorUsedError; + @JsonKey(name: "personal_site_link") + String? get personalSiteLink => throw _privateConstructorUsedError; + @JsonKey(name: "request_state") + String? get requestState => throw _privateConstructorUsedError; + + /// Serializes this EducationResource to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of EducationResource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $EducationResourceCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $EducationResourceCopyWith<$Res> { + factory $EducationResourceCopyWith( + EducationResource value, $Res Function(EducationResource) then) = + _$EducationResourceCopyWithImpl<$Res, EducationResource>; + @useResult + $Res call( + {@JsonKey(name: "has_medical_education") int? hasMedical_Education, + @JsonKey(name: "vk_link") String? vkLink, + @JsonKey(name: "insta_link") String? instaLink, + @JsonKey(name: "fb_link") String? fbLink, + @JsonKey(name: "tg_link") String? tgLink, + @JsonKey(name: "personal_site_link") String? personalSiteLink, + @JsonKey(name: "request_state") String? requestState}); +} + +/// @nodoc +class _$EducationResourceCopyWithImpl<$Res, $Val extends EducationResource> + implements $EducationResourceCopyWith<$Res> { + _$EducationResourceCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of EducationResource + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? hasMedical_Education = freezed, + Object? vkLink = freezed, + Object? instaLink = freezed, + Object? fbLink = freezed, + Object? tgLink = freezed, + Object? personalSiteLink = freezed, + Object? requestState = freezed, + }) { + return _then(_value.copyWith( + hasMedical_Education: freezed == hasMedical_Education + ? _value.hasMedical_Education + : hasMedical_Education // ignore: cast_nullable_to_non_nullable + as int?, + vkLink: freezed == vkLink + ? _value.vkLink + : vkLink // ignore: cast_nullable_to_non_nullable + as String?, + instaLink: freezed == instaLink + ? _value.instaLink + : instaLink // ignore: cast_nullable_to_non_nullable + as String?, + fbLink: freezed == fbLink + ? _value.fbLink + : fbLink // ignore: cast_nullable_to_non_nullable + as String?, + tgLink: freezed == tgLink + ? _value.tgLink + : tgLink // ignore: cast_nullable_to_non_nullable + as String?, + personalSiteLink: freezed == personalSiteLink + ? _value.personalSiteLink + : personalSiteLink // ignore: cast_nullable_to_non_nullable + as String?, + requestState: freezed == requestState + ? _value.requestState + : requestState // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$EducationResourceImplCopyWith<$Res> + implements $EducationResourceCopyWith<$Res> { + factory _$$EducationResourceImplCopyWith(_$EducationResourceImpl value, + $Res Function(_$EducationResourceImpl) then) = + __$$EducationResourceImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: "has_medical_education") int? hasMedical_Education, + @JsonKey(name: "vk_link") String? vkLink, + @JsonKey(name: "insta_link") String? instaLink, + @JsonKey(name: "fb_link") String? fbLink, + @JsonKey(name: "tg_link") String? tgLink, + @JsonKey(name: "personal_site_link") String? personalSiteLink, + @JsonKey(name: "request_state") String? requestState}); +} + +/// @nodoc +class __$$EducationResourceImplCopyWithImpl<$Res> + extends _$EducationResourceCopyWithImpl<$Res, _$EducationResourceImpl> + implements _$$EducationResourceImplCopyWith<$Res> { + __$$EducationResourceImplCopyWithImpl(_$EducationResourceImpl _value, + $Res Function(_$EducationResourceImpl) _then) + : super(_value, _then); + + /// Create a copy of EducationResource + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? hasMedical_Education = freezed, + Object? vkLink = freezed, + Object? instaLink = freezed, + Object? fbLink = freezed, + Object? tgLink = freezed, + Object? personalSiteLink = freezed, + Object? requestState = freezed, + }) { + return _then(_$EducationResourceImpl( + hasMedical_Education: freezed == hasMedical_Education + ? _value.hasMedical_Education + : hasMedical_Education // ignore: cast_nullable_to_non_nullable + as int?, + vkLink: freezed == vkLink + ? _value.vkLink + : vkLink // ignore: cast_nullable_to_non_nullable + as String?, + instaLink: freezed == instaLink + ? _value.instaLink + : instaLink // ignore: cast_nullable_to_non_nullable + as String?, + fbLink: freezed == fbLink + ? _value.fbLink + : fbLink // ignore: cast_nullable_to_non_nullable + as String?, + tgLink: freezed == tgLink + ? _value.tgLink + : tgLink // ignore: cast_nullable_to_non_nullable + as String?, + personalSiteLink: freezed == personalSiteLink + ? _value.personalSiteLink + : personalSiteLink // ignore: cast_nullable_to_non_nullable + as String?, + requestState: freezed == requestState + ? _value.requestState + : requestState // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$EducationResourceImpl implements _EducationResource { + const _$EducationResourceImpl( + {@JsonKey(name: "has_medical_education") this.hasMedical_Education, + @JsonKey(name: "vk_link") this.vkLink, + @JsonKey(name: "insta_link") this.instaLink, + @JsonKey(name: "fb_link") this.fbLink, + @JsonKey(name: "tg_link") this.tgLink, + @JsonKey(name: "personal_site_link") this.personalSiteLink, + @JsonKey(name: "request_state") this.requestState}); + + factory _$EducationResourceImpl.fromJson(Map json) => + _$$EducationResourceImplFromJson(json); + + @override + @JsonKey(name: "has_medical_education") + final int? hasMedical_Education; + @override + @JsonKey(name: "vk_link") + final String? vkLink; + @override + @JsonKey(name: "insta_link") + final String? instaLink; + @override + @JsonKey(name: "fb_link") + final String? fbLink; + @override + @JsonKey(name: "tg_link") + final String? tgLink; + @override + @JsonKey(name: "personal_site_link") + final String? personalSiteLink; + @override + @JsonKey(name: "request_state") + final String? requestState; + + @override + String toString() { + return 'EducationResource(hasMedical_Education: $hasMedical_Education, vkLink: $vkLink, instaLink: $instaLink, fbLink: $fbLink, tgLink: $tgLink, personalSiteLink: $personalSiteLink, requestState: $requestState)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$EducationResourceImpl && + (identical(other.hasMedical_Education, hasMedical_Education) || + other.hasMedical_Education == hasMedical_Education) && + (identical(other.vkLink, vkLink) || other.vkLink == vkLink) && + (identical(other.instaLink, instaLink) || + other.instaLink == instaLink) && + (identical(other.fbLink, fbLink) || other.fbLink == fbLink) && + (identical(other.tgLink, tgLink) || other.tgLink == tgLink) && + (identical(other.personalSiteLink, personalSiteLink) || + other.personalSiteLink == personalSiteLink) && + (identical(other.requestState, requestState) || + other.requestState == requestState)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, hasMedical_Education, vkLink, + instaLink, fbLink, tgLink, personalSiteLink, requestState); + + /// Create a copy of EducationResource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$EducationResourceImplCopyWith<_$EducationResourceImpl> get copyWith => + __$$EducationResourceImplCopyWithImpl<_$EducationResourceImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$EducationResourceImplToJson( + this, + ); + } +} + +abstract class _EducationResource implements EducationResource { + const factory _EducationResource( + {@JsonKey(name: "has_medical_education") final int? hasMedical_Education, + @JsonKey(name: "vk_link") final String? vkLink, + @JsonKey(name: "insta_link") final String? instaLink, + @JsonKey(name: "fb_link") final String? fbLink, + @JsonKey(name: "tg_link") final String? tgLink, + @JsonKey(name: "personal_site_link") final String? personalSiteLink, + @JsonKey(name: "request_state") + final String? requestState}) = _$EducationResourceImpl; + + factory _EducationResource.fromJson(Map json) = + _$EducationResourceImpl.fromJson; + + @override + @JsonKey(name: "has_medical_education") + int? get hasMedical_Education; + @override + @JsonKey(name: "vk_link") + String? get vkLink; + @override + @JsonKey(name: "insta_link") + String? get instaLink; + @override + @JsonKey(name: "fb_link") + String? get fbLink; + @override + @JsonKey(name: "tg_link") + String? get tgLink; + @override + @JsonKey(name: "personal_site_link") + String? get personalSiteLink; + @override + @JsonKey(name: "request_state") + String? get requestState; + + /// Create a copy of EducationResource + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$EducationResourceImplCopyWith<_$EducationResourceImpl> get copyWith => + throw _privateConstructorUsedError; +} + ProfileDetailsModel _$ProfileDetailsModelFromJson(Map json) { return _ProfileDetailsModel.fromJson(json); } diff --git a/lib/features/profile/models/profile_model.g.dart b/lib/features/profile/models/profile_model.g.dart index 47755454..f22b5656 100644 --- a/lib/features/profile/models/profile_model.g.dart +++ b/lib/features/profile/models/profile_model.g.dart @@ -34,6 +34,10 @@ _$ProfileModelImpl _$$ProfileModelImplFromJson(Map json) => subscribers: (json['subscribers'] as List) .map((e) => ProfilePreviewModel.fromJson(e as Map)) .toList(), + educationResource: json['education_resource'] == null + ? null + : EducationResource.fromJson( + json['education_resource'] as Map), ); Map _$$ProfileModelImplToJson(_$ProfileModelImpl instance) => @@ -52,6 +56,31 @@ Map _$$ProfileModelImplToJson(_$ProfileModelImpl instance) => 'badges': instance.badges, 'subscriptions': instance.subscriptions, 'subscribers': instance.subscribers, + 'education_resource': instance.educationResource, + }; + +_$EducationResourceImpl _$$EducationResourceImplFromJson( + Map json) => + _$EducationResourceImpl( + hasMedical_Education: (json['has_medical_education'] as num?)?.toInt(), + vkLink: json['vk_link'] as String?, + instaLink: json['insta_link'] as String?, + fbLink: json['fb_link'] as String?, + tgLink: json['tg_link'] as String?, + personalSiteLink: json['personal_site_link'] as String?, + requestState: json['request_state'] as String?, + ); + +Map _$$EducationResourceImplToJson( + _$EducationResourceImpl instance) => + { + 'has_medical_education': instance.hasMedical_Education, + 'vk_link': instance.vkLink, + 'insta_link': instance.instaLink, + 'fb_link': instance.fbLink, + 'tg_link': instance.tgLink, + 'personal_site_link': instance.personalSiteLink, + 'request_state': instance.requestState, }; _$ProfileDetailsModelImpl _$$ProfileDetailsModelImplFromJson( diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 2abd3feb..36d6628a 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -27,6 +27,7 @@ import 'package:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; class ProfileScreen extends StatefulWidget { final Author? author; @@ -460,11 +461,38 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ - _socialLinkButton(() {}, 'assets/icons/vk.svg'), + _socialLinkButton( + () { + launchUrl( + Uri.parse( + state.profile.educationResource?.vkLink ?? '', + ), + ); + }, + 'assets/icons/vk.svg', + ), const SizedBox(width: 12), - _socialLinkButton(() {}, 'assets/icons/telegram.svg'), + _socialLinkButton( + () { + launchUrl( + Uri.parse( + state.profile.educationResource?.tgLink ?? '', + ), + ); + }, + 'assets/icons/telegram.svg', + ), const SizedBox(width: 12), - _socialLinkButton(() {}, 'assets/icons/web.svg'), + _socialLinkButton( + () { + launchUrl( + Uri.parse( + state.profile.educationResource?.personalSiteLink ?? '', + ), + ); + }, + 'assets/icons/web.svg', + ), ], ), ), From 5e6f4cc84b1b3690e31264e0e89b41c574ac7812 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Dec 2024 01:29:02 +0300 Subject: [PATCH 10/12] fix: feed like sync --- .../.kotlin/errors/errors-1733227862976.log | 4 + lib/core/app_router.dart | 4 +- .../auth/auth_screen/auth_screen.dart | 3 + .../presentation/pages/blog_post_screen.dart | 2 +- .../bloc/feed_cubit/feed_cubit.dart | 16 + .../feed/presentation/pages/feed_screen.dart | 2 +- .../widgets/blog_post_widget.dart | 371 +++++++++--------- .../presentation/pages/profile_screen.dart | 12 +- lib/main.dart | 2 +- 9 files changed, 221 insertions(+), 195 deletions(-) create mode 100644 android/.kotlin/errors/errors-1733227862976.log diff --git a/android/.kotlin/errors/errors-1733227862976.log b/android/.kotlin/errors/errors-1733227862976.log new file mode 100644 index 00000000..1219b509 --- /dev/null +++ b/android/.kotlin/errors/errors-1733227862976.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 6facfbb2..c7d39c15 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/auth_screen/auth_screen.dart'; import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; @@ -40,7 +41,6 @@ import 'package:eatvio_app/features/settings/presentation/pages/physical_form_sc import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class AppRouter { @@ -77,7 +77,7 @@ class AppRouter { path: '/my-profile-screen', name: 'myProfileScreen', redirect: (context, state) async { - await context.read().runInitialization(); + await sl().runInitialization(); return null; }, pageBuilder: (context, state) => NoTransitionPage(child: ProfileScreen()), diff --git a/lib/features/auth/auth_screen/auth_screen.dart b/lib/features/auth/auth_screen/auth_screen.dart index 87bc13dd..220d61a5 100644 --- a/lib/features/auth/auth_screen/auth_screen.dart +++ b/lib/features/auth/auth_screen/auth_screen.dart @@ -1,9 +1,11 @@ import 'dart:convert'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -191,6 +193,7 @@ class _AuthScreenState extends State { final token = res['token']; await TokenStorage.saveTokenInStorage(token); GoRouter.of(context).go('/my-profile-screen'); + sl().getMyProfile(); } else { showDialog( context: context, 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 dd9b1c78..3f88ece9 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -304,7 +304,7 @@ class _BlogPostScreenState extends State { isLiked ? await context.read().likePost(widget.blogPost.id) : await context.read().unlikedPost(widget.blogPost.id); - await context.read().loadFeedBlogPostList(); + sl().updateFeedBlogPostList(widget.blogPost, isLiked); }, ), SizedBox(width: 8), diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 802f9a18..2af91d22 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,6 +14,21 @@ class FeedCubit extends Cubit { required this.feedRepository, }) : super(const FeedState.loading()); + updateFeedBlogPostList(FeedBlogPostModel feedBlogPostModel, bool wasLikedByMe) { + List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; + final int index = feedBlogPostList.indexOf(feedBlogPostModel); + feedBlogPostList[index] = feedBlogPostModel.copyWith( + wasLikedByMe: wasLikedByMe, + totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, + ); + emit( + FeedState.loaded( + feedBlogPostList: feedBlogPostList, + isLoading: isLoading, + ), + ); + } + loadFeedBlogPostList({ String? query, }) async { diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 611a9015..c439de95 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -184,7 +184,7 @@ class _FeedScreenState extends State { if (index == state.feedBlogPostList.length) { return const Loader(); } - return BlogPostWidget(feedBlogPostModel: state.feedBlogPostList[index]); + return BlogPostWidget(index: index); }, separatorBuilder: (context, index) { return SizedBox(height: 8); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index ee2d66aa..9d796fa6 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -4,9 +4,9 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,11 +16,11 @@ import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { - final FeedBlogPostModel feedBlogPostModel; + final int index; const BlogPostWidget({ super.key, - required this.feedBlogPostModel, + required this.index, }); @override @@ -31,206 +31,209 @@ class _BlogPostWidgetState extends State { late bool isLiked; late int totalLikes; - @override - void initState() { - isLiked = widget.feedBlogPostModel.wasLikedByMe; - totalLikes = widget.feedBlogPostModel.totalLikes; - super.initState(); - } - @override Widget build(BuildContext context) { - return InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: widget.feedBlogPostModel, - ), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( + return BlocBuilder( + builder: (BuildContext context, FeedState state) { + if (state is FeedStateLoaded) { + isLiked = state.feedBlogPostList[widget.index].wasLikedByMe; + totalLikes = state.feedBlogPostList[widget.index].totalLikes; + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => GoRouter.of(context).pushNamed( + 'blogPostScreen', + extra: state.feedBlogPostList[widget.index], + ), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CircleAvatar( - radius: 20, - backgroundColor: Colors.black12, - foregroundImage: NetworkImage( - widget.feedBlogPostModel.author.authorProfileIcon, + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Colors.black12, + foregroundImage: NetworkImage( + state.feedBlogPostList[widget.index].author.authorProfileIcon, + ), + ), + SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.feedBlogPostList[widget.index].author.authorName, + style: TextStyle( + height: 1, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + '${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyle( + height: 1, + color: Colors.black45, + ), + ), + ], + ), + const Spacer(), + if (!state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe && !state.feedBlogPostList[widget.index].author.authorIsMe) + SubscribeButtonWidget( + slug: state.feedBlogPostList[widget.index].author.authorSlug, + ), + ], ), ), - SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.feedBlogPostModel.author.authorName, + if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + state.feedBlogPostList[widget.index].title, + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ), + if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty) SizedBox(height: 8), + if (state.feedBlogPostList[widget.index].description.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Html( + data: state.feedBlogPostList[widget.index].description.length > 200 + ? state.feedBlogPostList[widget.index].description.substring(0, 200) + : state.feedBlogPostList[widget.index].description, + style: { + "*": Style( + color: Colors.black87, + ), + }, + ), + ), + if (state.feedBlogPostList[widget.index].description.length > 200) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Читать полностью...', style: TextStyle( height: 1, - color: Colors.black87, - fontWeight: FontWeight.w500, + color: appTheme.blueA400, + decoration: TextDecoration.underline, ), ), - const SizedBox(height: 4), - Text( - '${calculateDifference(widget.feedBlogPostModel.createdAt)} в ${widget.feedBlogPostModel.createdAt.hour.toString().padLeft(2, '0')}:${widget.feedBlogPostModel.createdAt.minute.toString().padLeft(2, '0')}', - style: TextStyle( - height: 1, - color: Colors.black45, + ), + if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) SizedBox(height: 8), + GestureDetector( + onTap: () { + context.pushTransparentRoute( + rootNavigator: true, + PhotoViewScreen( + imageUrls: [baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image], + index: 0, ), - ), - ], - ), - const Spacer(), - if (!widget.feedBlogPostModel.author.isEntitySubscribedByMe && !widget.feedBlogPostModel.author.authorIsMe) - SubscribeButtonWidget( - slug: widget.feedBlogPostModel.author.authorSlug, - ), - ], - ), - ), - if (widget.feedBlogPostModel.title.isNotEmpty || widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - widget.feedBlogPostModel.title, - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - ), - if (widget.feedBlogPostModel.title.isNotEmpty && widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 8), - if (widget.feedBlogPostModel.description.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Html( - data: widget.feedBlogPostModel.description.length > 200 - ? widget.feedBlogPostModel.description.substring(0, 200) - : widget.feedBlogPostModel.description, - style: { - "*": Style( - color: Colors.black87, - ), - }, - ), - ), - if (widget.feedBlogPostModel.description.length > 200) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Читать полностью...', - style: TextStyle( - height: 1, - color: appTheme.blueA400, - decoration: TextDecoration.underline, - ), - ), - ), - if (widget.feedBlogPostModel.title.isNotEmpty || widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 8), - GestureDetector( - onTap: () { - context.pushTransparentRoute( - rootNavigator: true, - PhotoViewScreen( - imageUrls: [baseUrl + '/storage/' + widget.feedBlogPostModel.image], - index: 0, - ), - ); - }, - child: Hero( - tag: baseUrl + '/storage/' + widget.feedBlogPostModel.image, - child: CachedNetworkImage( - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - imageUrl: baseUrl + '/storage/' + widget.feedBlogPostModel.image, - errorWidget: (context, str, object) => SizedBox.shrink(), - ), - ), - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - FeedIconButton( - icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, - text: totalLikes.toString(), - iconColor: isLiked ? Colors.red.withOpacity(0.8) : null, - backgroundColor: isLiked ? Colors.red.withOpacity(0.1) : null, - textColor: isLiked ? Colors.red.withOpacity(0.8) : null, - onTap: () async { - setState(() { - isLiked = !isLiked; - isLiked ? totalLikes++ : totalLikes--; - }); - isLiked - ? await context.read().likePost(widget.feedBlogPostModel.id) - : await context.read().unlikedPost(widget.feedBlogPostModel.id); - await context.read().loadFeedBlogPostList(); + ); }, - ), - SizedBox(width: 8), - FeedIconButton( - svgIcon: SvgPicture.asset( - 'assets/icons/comment.svg', - height: 20, - width: 20, - colorFilter: ColorFilter.mode( - appTheme.gray600, - BlendMode.srcIn, - ), - ), - text: widget.feedBlogPostModel.totalComments.toString(), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: widget.feedBlogPostModel, - ), - ), - SizedBox(width: 8), - FeedIconButton( - svgIcon: SvgPicture.asset( - height: 20, - width: 20, - 'assets/icons/share.svg', - colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), - ), - onTap: () => Share.shareUri( - Uri.parse( - baseUrl + '/ru/blog_post/' + widget.feedBlogPostModel.id.toString(), + child: Hero( + tag: baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image, + child: CachedNetworkImage( + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + imageUrl: baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image, + errorWidget: (context, str, object) => SizedBox.shrink(), ), ), ), - Spacer(), - Icon( - Icons.visibility_rounded, - size: 16, - color: appTheme.gray500, - ), - SizedBox(width: 4), - Text( - widget.feedBlogPostModel.totalViews.toString(), - style: TextStyle( - fontSize: 12, - color: appTheme.gray500, - fontWeight: FontWeight.w400, + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + FeedIconButton( + icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, + text: totalLikes.toString(), + iconColor: isLiked ? Colors.red.withOpacity(0.8) : null, + backgroundColor: isLiked ? Colors.red.withOpacity(0.1) : null, + textColor: isLiked ? Colors.red.withOpacity(0.8) : null, + onTap: () async { + setState(() { + isLiked = !isLiked; + isLiked ? totalLikes++ : totalLikes--; + }); + isLiked + ? await context.read().likePost(state.feedBlogPostList[widget.index].id) + : await context.read().unlikedPost(state.feedBlogPostList[widget.index].id); + await context.read().loadFeedBlogPostList(); + }, + ), + SizedBox(width: 8), + FeedIconButton( + svgIcon: SvgPicture.asset( + 'assets/icons/comment.svg', + height: 20, + width: 20, + colorFilter: ColorFilter.mode( + appTheme.gray600, + BlendMode.srcIn, + ), + ), + text: state.feedBlogPostList[widget.index].totalComments.toString(), + onTap: () => GoRouter.of(context).pushNamed( + 'blogPostScreen', + extra: state.feedBlogPostList[widget.index], + ), + ), + SizedBox(width: 8), + FeedIconButton( + svgIcon: SvgPicture.asset( + height: 20, + width: 20, + 'assets/icons/share.svg', + colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), + ), + onTap: () => Share.shareUri( + Uri.parse( + baseUrl + '/ru/blog_post/' + state.feedBlogPostList[widget.index].id.toString(), + ), + ), + ), + Spacer(), + Icon( + Icons.visibility_rounded, + size: 16, + color: appTheme.gray500, + ), + SizedBox(width: 4), + Text( + state.feedBlogPostList[widget.index].totalViews.toString(), + style: TextStyle( + fontSize: 12, + color: appTheme.gray500, + fontWeight: FontWeight.w400, + ), + ), + ], ), ), + SizedBox(height: 8), ], ), ), - SizedBox(height: 8), - ], - ), - ), + ); + } + return const SizedBox.shrink(); + }, ); } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 36d6628a..81a7cc7a 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -122,11 +122,11 @@ class _ProfileScreenState extends State { ), ), _subscriptionsAndSubscribers(state), - if (state.profile.badges.isNotEmpty) _badges(state), - _socialLinks(state), - _education(state), - _diplomasAndCertificates(state), - _activePlans(state), + if (state.profile.badges.isNotEmpty) Padding(padding: EdgeInsets.only(bottom: 16), child: _badges(state)), + if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _socialLinks(state)), + if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _education(state)), + if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _diplomasAndCertificates(state)), + if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _activePlans(state)), _blogPosts(state), ], ), @@ -706,7 +706,7 @@ class _ProfileScreenState extends State { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => BlogPostWidget( - feedBlogPostModel: state.blogPosts[index], + index: index, ), separatorBuilder: (context, index) => SizedBox(height: 8), itemCount: state.blogPosts.length, diff --git a/lib/main.dart b/lib/main.dart index 195c45de..97522447 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()..runInitialization()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), From 480a26fe78818b817f3db76704283f605e490d3f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Dec 2024 13:11:13 +0300 Subject: [PATCH 11/12] feat(diary): alert chart --- .../widgets/calories_chart_widget.dart | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/features/diary/presentation/widgets/calories_chart_widget.dart b/lib/features/diary/presentation/widgets/calories_chart_widget.dart index 73e40206..d59d3b24 100644 --- a/lib/features/diary/presentation/widgets/calories_chart_widget.dart +++ b/lib/features/diary/presentation/widgets/calories_chart_widget.dart @@ -20,19 +20,31 @@ class CaloriesChartWidget extends StatelessWidget { ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - 'Ваш КБЖУ от дневной\nсуточной нормы', - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFF323F4B), - fontWeight: FontWeight.w500, - fontSize: 18, - height: 1, + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text( + 'График отображает сколько калорий вам нужно ежедневно для похуденияи поддержания веса, согласно формуле Харриса-Бенедикта.\nИзменить коэффициент физической нагрузки можно в настройках.', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xFF323F4B), + height: 1.2, + ), + ), + ), + ); + }, + child: Icon( + Icons.help_outline_rounded, + color: appTheme.blueA400, + size: 28, ), ), - const SizedBox(height: 48), + const SizedBox(height: 12), Stack( alignment: Alignment.bottomCenter, children: [ @@ -60,7 +72,9 @@ class CaloriesChartWidget extends StatelessWidget { RangePointer( value: (state.nutrition.dailyNutritionIntake.carbohydrates / state.nutrition.dailyNutritionRecommendation.carbohydrates) * 100, width: 0.16, - color: state.nutrition.dailyNutritionIntake.carbohydrates < state.nutrition.dailyNutritionRecommendation.carbohydrates ? appTheme.blueA400 : Colors.red, + color: state.nutrition.dailyNutritionIntake.carbohydrates < state.nutrition.dailyNutritionRecommendation.carbohydrates + ? appTheme.blueA400 + : Colors.red, sizeUnit: GaugeSizeUnit.factor, enableAnimation: true, cornerStyle: CornerStyle.bothCurve, @@ -78,7 +92,9 @@ class CaloriesChartWidget extends StatelessWidget { RangePointer( value: (state.nutrition.dailyNutritionIntake.fats / state.nutrition.dailyNutritionRecommendation.fats) * 100, width: 0.16, - color: state.nutrition.dailyNutritionIntake.fats < state.nutrition.dailyNutritionRecommendation.fats ? appTheme.blueA400 : Colors.red, + color: state.nutrition.dailyNutritionIntake.fats < state.nutrition.dailyNutritionRecommendation.fats + ? appTheme.blueA400 + : Colors.red, sizeUnit: GaugeSizeUnit.factor, enableAnimation: true, cornerStyle: CornerStyle.bothCurve, @@ -97,7 +113,9 @@ class CaloriesChartWidget extends StatelessWidget { RangePointer( value: (state.nutrition.dailyNutritionIntake.proteins / state.nutrition.dailyNutritionRecommendation.proteins) * 100, width: 0.16, - color: state.nutrition.dailyNutritionIntake.proteins < state.nutrition.dailyNutritionRecommendation.proteins ? appTheme.blueA400 : Colors.red, + color: state.nutrition.dailyNutritionIntake.proteins < state.nutrition.dailyNutritionRecommendation.proteins + ? appTheme.blueA400 + : Colors.red, sizeUnit: GaugeSizeUnit.factor, enableAnimation: true, cornerStyle: CornerStyle.bothCurve, @@ -116,7 +134,9 @@ class CaloriesChartWidget extends StatelessWidget { RangePointer( value: (state.nutrition.dailyNutritionIntake.energy / state.nutrition.dailyNutritionRecommendation.energy) * 100, width: 0.16, - color: state.nutrition.dailyNutritionIntake.energy < state.nutrition.dailyNutritionRecommendation.energy ? appTheme.blueA400 : Colors.red, + color: state.nutrition.dailyNutritionIntake.energy < state.nutrition.dailyNutritionRecommendation.energy + ? appTheme.blueA400 + : Colors.red, sizeUnit: GaugeSizeUnit.factor, enableAnimation: true, cornerStyle: CornerStyle.bothCurve, From cb1b480c428dc08584d8a3c47d0878de0e3a5ad5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Dec 2024 15:15:28 +0300 Subject: [PATCH 12/12] feat(profile): delete photo --- .../bloc/profile_cubit/profile_cubit.dart | 5 ++++ .../presentation/pages/profile_screen.dart | 6 ++++- .../repositories/profile_repository.dart | 8 ++++++ .../profile/services/profile_service.dart | 3 +++ .../profile/services/profile_service.g.dart | 25 +++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 5a322425..ad975da2 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -92,4 +92,9 @@ class ProfileCubit extends Cubit { await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file)); return await getMyProfile(); } + + deletePhoto() async { + await profileRepository.deletePhoto(); + return await getMyProfile(); + } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 81a7cc7a..2647b989 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -737,7 +737,7 @@ class _ProfileScreenState extends State { child: Row( children: [ PopupMenuButton( - onSelected: (value) { + onSelected: (value) async { if (value == 'view') { context.pushTransparentRoute( rootNavigator: true, @@ -797,6 +797,10 @@ class _ProfileScreenState extends State { ), ); } + if (value == 'delete') { + await profileCubit.deletePhoto(); + await sl().getMyProfile(); + } }, itemBuilder: (context) => [ PopupMenuItem( diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 96825109..21f5e7b3 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -163,4 +163,12 @@ class ProfileRepository { rethrow; } } + + Future deletePhoto() async { + try { + return await profileService.deletePhoto(); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 8cab7056..d357ea3a 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -96,4 +96,7 @@ abstract class ProfileService { Future uploadPhoto({ @Part(name: 'photo') required File photo, }); + + @DELETE('/api/profile/photo/delete') + Future deletePhoto(); } diff --git a/lib/features/profile/services/profile_service.g.dart b/lib/features/profile/services/profile_service.g.dart index a0e72b67..e8214593 100644 --- a/lib/features/profile/services/profile_service.g.dart +++ b/lib/features/profile/services/profile_service.g.dart @@ -571,6 +571,31 @@ class _ProfileService implements ProfileService { await _dio.fetch(_options); } + @override + Future deletePhoto() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'DELETE', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/profile/photo/delete', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes ||