From 2ba130f875834b8f831cc96f90c773e97cb0b4fb Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 16 Apr 2025 13:24:43 +0300 Subject: [PATCH] feat(feed): views, more options --- .../presentation/pages/blog_post_screen.dart | 1 + .../presentation/pages/about_diet_screen.dart | 3 + .../bloc/feed_cubit/feed_cubit.dart | 5 ++ .../widgets/blog_post_widget.dart | 42 +++++++++- .../presentation/widgets/diets_widget.dart | 36 ++++++++- .../presentation/widgets/plan_widget.dart | 36 ++++++++- .../presentation/widgets/recipes_widget.dart | 36 ++++++++- .../feed/repositories/feed_repository.dart | 32 ++++++++ lib/features/feed/services/feed_service.dart | 20 +++++ .../feed/services/feed_service.g.dart | 76 +++++++++++++++++++ .../presentation/pages/about_plan_screen.dart | 2 + .../presentation/about_recipe_screen.dart | 2 + 12 files changed, 284 insertions(+), 7 deletions(-) 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 29f2f5bc..4f6a0ad0 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -41,6 +41,7 @@ class _BlogPostScreenState extends State { void initState() { isLiked = widget.blogPost.wasLikedByMe; totalLikes = widget.blogPost.totalLikes; + context.read().viewPost(widget.blogPost.id); super.initState(); } diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 1d480f9c..0ca2c959 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -23,6 +23,7 @@ import 'package:eatvio_app/features/diets/presentation/widgets/start_diet_bottom import 'package:eatvio_app/features/feed/models/diets_pagination_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'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -55,9 +56,11 @@ class _AboutDietScreenState extends State { final textEditingController = TextEditingController(); bool isMoreOptionsVisible = false; bool _jumped = false; + @override void initState() { super.initState(); + context.read().viewDiet(widget.diet.id); scrollController.addListener(() { if (isMoreOptionsVisible) { setState(() { 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 51552e2b..15b51e8f 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -80,4 +80,9 @@ class FeedCubit extends Cubit { likePost(int id) async => await feedRepository.likeBlogPost(id: id); unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id); + + viewPost(int id) async => await feedRepository.viewPost(id); + viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); + viewDiet(int id) async => await feedRepository.viewDiet(id); + viewPlan(String slug) async => await feedRepository.viewPlan(slug); } diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index d670a7c9..089ad1d0 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -1,9 +1,11 @@ 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/enums/complained_content.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/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -12,6 +14,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_w import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; @@ -31,6 +34,7 @@ class BlogPostWidget extends StatefulWidget { class _BlogPostWidgetState extends State { late bool isLiked; late int totalLikes; + bool isMoreOptionsVisible = false; @override Widget build(BuildContext context) { @@ -110,9 +114,37 @@ class _BlogPostWidgetState extends State { const Spacer(), if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && !state.feedBlogPostList[widget.index].author.authorIsMe) - SubscribeButtonWidget( - slug: state.feedBlogPostList[widget.index].author.authorSlug, + Padding( + padding: const EdgeInsets.only(right: 8), + child: SubscribeButtonWidget( + slug: state.feedBlogPostList[widget.index].author.authorSlug, + ), ), + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.posts, + path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}', + id: state.feedBlogPostList[widget.index].id, + ), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), + child: InkWell( + onTap: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + child: SizedBox( + width: 32, + height: 24, + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + child: const Icon(Icons.more_horiz), + ), + ), + ), + ), ], ), ), @@ -129,7 +161,8 @@ class _BlogPostWidgetState extends State { ), ), ), - if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty) const SizedBox(height: 8), + if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty) + const SizedBox(height: 8), if (state.feedBlogPostList[widget.index].description.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 4), @@ -157,7 +190,8 @@ class _BlogPostWidgetState extends State { ), ), ), - if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) const SizedBox(height: 8), + if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) + const SizedBox(height: 8), GestureDetector( onTap: () { context.pushTransparentRoute( diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 4d1344eb..b5830473 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,9 +1,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -11,6 +13,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; @@ -29,6 +32,8 @@ class DietsWidget extends StatefulWidget { } class _DietsWidgetState extends State { + bool isMoreOptionsVisible = false; + @override Widget build(BuildContext context) { return InkWell( @@ -60,7 +65,7 @@ class _DietsWidgetState extends State { ), ), Positioned( - right: 14, + left: 14, child: BookmarkButton( onTap: () async { if (widget.diet.isFavorite) { @@ -77,6 +82,35 @@ class _DietsWidgetState extends State { size: 19, ), ), + Positioned( + right: 12, + top: 12, + child: PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + path: 'diet/${widget.diet.id}', + complainedContentType: ComplainedContent.diet, + id: widget.diet.id, + ), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), + child: InkWell( + onTap: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + child: SizedBox( + width: 32, + height: 24, + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + child: const Icon(Icons.more_horiz), + ), + ), + ), + ), + ), Positioned( bottom: 16, child: SizedBox( diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index e1f26bed..0f6e135f 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,10 +1,12 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -14,6 +16,7 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; @@ -32,6 +35,8 @@ class PlanWidget extends StatefulWidget { } class _PlanWidgetState extends State { + bool isMoreOptionsVisible = false; + @override Widget build(BuildContext context) { final s = S.of(context); @@ -64,7 +69,7 @@ class _PlanWidgetState extends State { ), ), Positioned( - right: 14, + left: 14, child: BookmarkButton( onTap: () async { if (widget.plan.isFavorite) { @@ -81,6 +86,35 @@ class _PlanWidgetState extends State { size: 19, ), ), + Positioned( + right: 12, + top: 12, + child: PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + path: 'plan/${widget.plan.id}', + complainedContentType: ComplainedContent.plan, + id: widget.plan.id, + ), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), + child: InkWell( + onTap: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + child: SizedBox( + width: 32, + height: 24, + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + child: const Icon(Icons.more_horiz), + ), + ), + ), + ), + ), Positioned( bottom: 16, child: SizedBox( diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index be5701df..30202813 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -2,10 +2,12 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; @@ -14,6 +16,7 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; @@ -33,6 +36,8 @@ class RecipesWidget extends StatefulWidget { } class _RecipesWidgetState extends State { + bool isMoreOptionsVisible = false; + @override Widget build(BuildContext context) { final s = S.of(context); @@ -93,7 +98,7 @@ class _RecipesWidgetState extends State { ), ), Positioned( - right: 14, + left: 14, child: BookmarkButton( onTap: () async { if (isFavorite) { @@ -110,6 +115,35 @@ class _RecipesWidgetState extends State { size: 19, ), ), + Positioned( + right: 12, + top: 12, + child: PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.recipe, + path: 'recipe/${widget.recipe.slug}', + slug: widget.recipe.slug, + ), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), + child: InkWell( + onTap: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + child: SizedBox( + width: 32, + height: 24, + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + child: const Icon(Icons.more_horiz), + ), + ), + ), + ), + ), ], ), const SizedBox(height: 12), diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 854ce4d3..c974407c 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -217,4 +217,36 @@ class FeedRepository { rethrow; } } + + Future viewPost(int id) async { + try { + return (await feedService.viewPost(id: id)); + } catch (e) { + rethrow; + } + } + + Future viewRecipe(String slug) async { + try { + return (await feedService.viewRecipe(slug: slug)); + } catch (e) { + rethrow; + } + } + + Future viewDiet(int id) async { + try { + return (await feedService.viewDiet(id: id)); + } catch (e) { + rethrow; + } + } + + Future viewPlan(String slug) async { + try { + return (await feedService.viewPlan(slug: slug)); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 9551f16b..fded5033 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -105,4 +105,24 @@ abstract class FeedService { @GET('/api/consultants/index') Future getConsultants(); + + @POST('/api/blogPosts/{id}/view') + Future viewPost({ + @Path() required int id, + }); + + @POST('/api/recipes/{slug}/view') + Future viewRecipe({ + @Path() required String slug, + }); + + @POST('/api/diets/{id}/view') + Future viewDiet({ + @Path() required int id, + }); + + @POST('/api/plans/{slug}/view') + Future viewPlan({ + @Path() required String slug, + }); } diff --git a/lib/features/feed/services/feed_service.g.dart b/lib/features/feed/services/feed_service.g.dart index 17825316..ffad9a2d 100644 --- a/lib/features/feed/services/feed_service.g.dart +++ b/lib/features/feed/services/feed_service.g.dart @@ -454,6 +454,82 @@ class _FeedService implements FeedService { return _value; } + @override + Future viewPost({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/${id}/view', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future viewRecipe({required String slug}) 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/recipes/${slug}/view', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future viewDiet({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/diets/${id}/view', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future viewPlan({required String slug}) 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/plans/${slug}/view', + 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/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 0590e1be..d64a7cbb 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -18,6 +18,7 @@ import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart import 'package:eatvio_app/features/feed/models/plan_pagination_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'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; @@ -53,6 +54,7 @@ class _AboutPlanScreenState extends State { @override void initState() { super.initState(); + context.read().viewPlan(widget.plan.slug); scrollController.addListener(() { if (isMoreOptionsVisible) { setState(() { diff --git a/lib/features/recipe/presentation/about_recipe_screen.dart b/lib/features/recipe/presentation/about_recipe_screen.dart index ee906963..9139e094 100644 --- a/lib/features/recipe/presentation/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/about_recipe_screen.dart @@ -19,6 +19,7 @@ import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart import 'package:eatvio_app/features/feed/models/recipes_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'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_state.dart'; @@ -56,6 +57,7 @@ class _AboutRecipeScreenState extends State { @override void initState() { super.initState(); + context.read().viewRecipe(widget.recipe.slug); scrollController.addListener(() { if (isMoreOptionsVisible) { setState(() {