feat(feed): views, more options

This commit is contained in:
mso7nueh
2025-04-16 13:24:43 +03:00
parent b0fb4b4727
commit 2ba130f875
12 changed files with 284 additions and 7 deletions

View File

@@ -41,6 +41,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
void initState() {
isLiked = widget.blogPost.wasLikedByMe;
totalLikes = widget.blogPost.totalLikes;
context.read<FeedCubit>().viewPost(widget.blogPost.id);
super.initState();
}

View File

@@ -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<AboutDietScreen> {
final textEditingController = TextEditingController();
bool isMoreOptionsVisible = false;
bool _jumped = false;
@override
void initState() {
super.initState();
context.read<FeedCubit>().viewDiet(widget.diet.id);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {

View File

@@ -80,4 +80,9 @@ class FeedCubit extends Cubit<FeedState> {
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);
}

View File

@@ -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<BlogPostWidget> {
late bool isLiked;
late int totalLikes;
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
@@ -110,9 +114,37 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
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<BlogPostWidget> {
),
),
),
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<BlogPostWidget> {
),
),
),
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(

View File

@@ -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<DietsWidget> {
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
return InkWell(
@@ -60,7 +65,7 @@ class _DietsWidgetState extends State<DietsWidget> {
),
),
Positioned(
right: 14,
left: 14,
child: BookmarkButton(
onTap: () async {
if (widget.diet.isFavorite) {
@@ -77,6 +82,35 @@ class _DietsWidgetState extends State<DietsWidget> {
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(

View File

@@ -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<PlanWidget> {
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -64,7 +69,7 @@ class _PlanWidgetState extends State<PlanWidget> {
),
),
Positioned(
right: 14,
left: 14,
child: BookmarkButton(
onTap: () async {
if (widget.plan.isFavorite) {
@@ -81,6 +86,35 @@ class _PlanWidgetState extends State<PlanWidget> {
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(

View File

@@ -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<RecipesWidget> {
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -93,7 +98,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
),
),
Positioned(
right: 14,
left: 14,
child: BookmarkButton(
onTap: () async {
if (isFavorite) {
@@ -110,6 +115,35 @@ class _RecipesWidgetState extends State<RecipesWidget> {
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),

View File

@@ -217,4 +217,36 @@ class FeedRepository {
rethrow;
}
}
Future<void> viewPost(int id) async {
try {
return (await feedService.viewPost(id: id));
} catch (e) {
rethrow;
}
}
Future<void> viewRecipe(String slug) async {
try {
return (await feedService.viewRecipe(slug: slug));
} catch (e) {
rethrow;
}
}
Future<void> viewDiet(int id) async {
try {
return (await feedService.viewDiet(id: id));
} catch (e) {
rethrow;
}
}
Future<void> viewPlan(String slug) async {
try {
return (await feedService.viewPlan(slug: slug));
} catch (e) {
rethrow;
}
}
}

View File

@@ -105,4 +105,24 @@ abstract class FeedService {
@GET('/api/consultants/index')
Future<FeedConsultantPaginationModel> getConsultants();
@POST('/api/blogPosts/{id}/view')
Future<void> viewPost({
@Path() required int id,
});
@POST('/api/recipes/{slug}/view')
Future<void> viewRecipe({
@Path() required String slug,
});
@POST('/api/diets/{id}/view')
Future<void> viewDiet({
@Path() required int id,
});
@POST('/api/plans/{slug}/view')
Future<void> viewPlan({
@Path() required String slug,
});
}

View File

@@ -454,6 +454,82 @@ class _FeedService implements FeedService {
return _value;
}
@override
Future<void> viewPost({required int id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(
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<void>(_options);
}
@override
Future<void> viewRecipe({required String slug}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(
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<void>(_options);
}
@override
Future<void> viewDiet({required int id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(
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<void>(_options);
}
@override
Future<void> viewPlan({required String slug}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(
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<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

View File

@@ -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<AboutPlanScreen> {
@override
void initState() {
super.initState();
context.read<FeedCubit>().viewPlan(widget.plan.slug);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {

View File

@@ -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<AboutRecipeScreen> {
@override
void initState() {
super.initState();
context.read<FeedCubit>().viewRecipe(widget.recipe.slug);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {